Testing MySQL Replication Connection Testing MySQL Replication Connection

    目前项目中使用了MySQL replication,并通过LVS对slaves进行负载均衡,数据库连接池使用的是c3p0。在使用过程中发现, LVS TCP timeout可能导致数据库连接被切断,从而应用程序中报数据库连接异常。
    ReplicationConnection内部保持了两个数据库连接,分别是masterConnection和slaveConnection。实际生效的连接取决于连接的readOnly属性,即readOnly ? currentConnection=slaveConnection : currentConnection=masterConnection。
    c3p0的提供了两种处理空闲连接的机制,对应的配置参数分别是:idleConnectionTestPeriod和maxIdleTime。但是这两种机制在默认情况下对ReplicationConnection不奏效。原因如下:

  • idleConnectionTestPeriod:c3p0在定期检查ReplicationConnection时只是检查了其内部的currentConnection,如果在某段时间内连接的readOnly属性为false,那么c3p0只会检查masterConnection,而不会检测slaveConnection是否仍然有效。
  • maxIdleTime:如果应用程序在一段内只以readOnly=false使用某个ReplicationConnection,那么c3p0不会认为该连接空闲,而实际上其内部的slaveConnection可能已经超时。

    默认情况下,c3p0使用DefaultConnectionTester(通过connectionTesterClassName配置)进行连接检查(基于Query),该类有以下两个比较重要的方法:

  • public int activeCheckConnection(Connection c, String query, Throwable[] rootCauseOutParamHolder):通常用于checkin,checkout和定期的连接检查。
  • public int statusOnException(Connection c, Throwable t, String query, Throwable[] rootCauseOutParamHolder):在连接抛异常的情况下,c3p0通过此方法确认连接本身是否可用(即是否需要销毁此连接)。

    需要注意的是,MySQL connector也提供了一个实现了c3p0的ConnectionTester接口的类:MysqlConnectionTester。该类使用com.mysql.jdbc.Connection的ping方法(相对于执行query,ping更轻量级)来确认连接是否正常。笔者认为该类仍然不能正确检测ReplicationConnection。

    以下是笔者实现的一个ConnectionTester,用于检查MySQL ReplicationConnection:

Java代码   收藏代码
  1. import java.sql.Connection;  
  2. import java.sql.ResultSet;  
  3. import java.sql.SQLException;  
  4. import java.sql.Statement;  
  5.   
  6. import org.slf4j.Logger;  
  7. import org.slf4j.LoggerFactory;  
  8.   
  9. import com.mchange.v2.c3p0.AbstractConnectionTester;  
  10. import com.mysql.jdbc.CommunicationsException;  
  11.   
  12.   
  13. public final class MysqlReplicationConnectionTester extends AbstractConnectionTester {  
  14.     //  
  15.     private static final Logger LOGGER = LoggerFactory.getLogger(MysqlReplicationConnectionTester.class);  
  16.       
  17.     //  
  18.     private static final long serialVersionUID = -7348778746126099053L;  
  19.       
  20.     //  
  21.     private static final String DEFAULT_QUERY = "SELECT 1";  
  22.       
  23.     public boolean equals(Object o) {  
  24.         return (o != null && o.getClass() == MysqlReplicationConnectionTester.class);  
  25.     }  
  26.   
  27.     public int hashCode() {  
  28.         return MysqlReplicationConnectionTester.class.getName().hashCode();  
  29.     }  
  30.           
  31.     public int activeCheckConnection(Connection c, String query, Throwable[] outParamCause) {  
  32.         //  
  33.         boolean readOnly = false;  
  34.         boolean needRestoreReadOnly = false;  
  35.         try {  
  36.             //  
  37.             readOnly = c.isReadOnly();  
  38.               
  39.             //  
  40.             int r = checkConnection(c, query, outParamCause, readOnly);  
  41.             if(r == CONNECTION_IS_OKAY) {  
  42.                 needRestoreReadOnly = true;  
  43.                 r = checkConnection(c, query, outParamCause, !readOnly);  
  44.             }  
  45.             return r;  
  46.         } catch(Exception e) {  
  47.             //  
  48.             LOGGER.warn("the connection: " + c + " was marked invalid", e);  
  49.             if (outParamCause != null) {  
  50.                 outParamCause[0] = e;  
  51.             }  
  52.             return CONNECTION_IS_INVALID;  
  53.         } finally {  
  54.             try {  
  55.                 if(needRestoreReadOnly) {  
  56.                     c.setReadOnly(readOnly);  
  57.                 }  
  58.             } catch (SQLException e) {  
  59.                 LOGGER.error("failed to restore read only: " + readOnly + " on connection", e);  
  60.             }  
  61.         }  
  62.     }  
  63.   
  64.     public int statusOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) {  
  65.         //  
  66.         int r = checkConnectionOnException(c, t, query, outParamCause);  
  67.           
  68.         //  
  69.         if(r != CONNECTION_IS_OKAY) {  
  70.             if (outParamCause != null) {  
  71.                 outParamCause[0] = t;  
  72.             }  
  73.         }  
  74.         return r;  
  75.     }  
  76.   
  77.     private int checkConnection(Connection c, String query, Throwable[] outParamCause, Boolean readOnly) {  
  78.         //  
  79.         if (query == null || query.equals("")) {  
  80.             query = DEFAULT_QUERY;  
  81.         }  
  82.           
  83.         //  
  84.         ResultSet rs = null;  
  85.         Statement stmt = null;  
  86.         try {  
  87.             //  
  88.             boolean ro = c.isReadOnly();  
  89.             if(readOnly != null && readOnly != ro) {  
  90.                 c.setReadOnly(readOnly);  
  91.             }  
  92.               
  93.             //  
  94.             if(LOGGER.isInfoEnabled()) {  
  95.                 LOGGER.info("testing connection: {} with query: {}, read only: {}"new Object[]{c, query, ro});  
  96.             }  
  97.               
  98.             //  
  99.             stmt = c.createStatement();  
  100.             rs = stmt.executeQuery(query);  
  101.             return CONNECTION_IS_OKAY;  
  102.         } catch (SQLException e) {  
  103.             LOGGER.warn("failed to test connection: " + c + " with query: " + query + ", state: " + e.getSQLState(), e);  
  104.             if (outParamCause != null) {  
  105.                 outParamCause[0] = e;  
  106.             }  
  107.             return CONNECTION_IS_INVALID;  
  108.         } catch (Exception e) {  
  109.             LOGGER.warn("failed to test connection: " + c + " with query: " + query, e);  
  110.             if (outParamCause != null) {  
  111.                 outParamCause[0] = e;  
  112.             }  
  113.             return CONNECTION_IS_INVALID;  
  114.         } finally {  
  115.             closeQuietly(rs);  
  116.             closeQuietly(stmt);  
  117.         }  
  118.     }  
  119.       
  120.     private int checkConnectionOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) {  
  121.         //  
  122.         if (t instanceof CommunicationsException) {  
  123.             return CONNECTION_IS_INVALID;  
  124.         }  
  125.           
  126.         //  
  127.         if (t instanceof SQLException) {  
  128.             final String sqlState = ((SQLException) t).getSQLState();  
  129.             if (sqlState != null && sqlState.startsWith("08")) {  
  130.                 return CONNECTION_IS_INVALID;  
  131.             } else {  
  132.                 return CONNECTION_IS_OKAY;  
  133.             }  
  134.         }  
  135.           
  136.         // Runtime/Unchecked?  
  137.         return CONNECTION_IS_INVALID;  
  138.     }  
  139.       
  140.     private void closeQuietly(ResultSet rs) {  
  141.         //  
  142.         if(rs == null) {  
  143.             return;  
  144.         }  
  145.           
  146.         //  
  147.         try {  
  148.             rs.close();  
  149.         } catch (SQLException e) {  
  150.             LOGGER.warn("failed to close result set", e);  
  151.         }  
  152.     }  
  153.       
  154.     private void closeQuietly(Statement stat) {  
  155.         //  
  156.         if(stat == null) {  
  157.             return;  
  158.         }  
  159.           
  160.         //  
  161.         try {  
  162.             stat.close();  
  163.         } catch (SQLException e) {  
  164.             LOGGER.warn("failed to close statement", e);  
  165.         }  
  166.         }  
  167. }  

    需要注意的是,以上代码适用于MySQL 5.0。对于MySQL 5.1,需要修改checkConnectionOnException方法,如下:

Java代码   收藏代码
  1.  if (t instanceof CommunicationsException || "com.mysql.jdbc.exceptions.jdbc4.CommunicationsException".equals(throwable.getClass().getName())) {  
  2.     return CONNECTION_IS_INVALID;  
  3. }  

    此外, 由于Spring Dao会转译SQLException,因此在Spring环境中,不能使用sqlState判断连接是否正常,而是需要使用基于query的方式,如下:

Java代码   收藏代码
  1. public int statusOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) {  
  2.     return checkConnection(c, query, outParamCause, null);  
  3. }  

猜你喜欢

转载自goahead2010.iteye.com/blog/1906633