目前项目中使用了MySQL replication,并通过LVS对slaves进行负载均衡,数据库连接池使用的是c3p0。在使用过程中发现, LVS TCP timeout可能导致数据库连接被切断,从而应用程序中报数据库连接异常。
ReplicationConnection内部保持了两个数据库连接,分别是masterConnection和slaveConnection。实际生效的连接取决于连接的readOnly属性,即readOnly ? currentConnection=slaveConnection : currentConnection=masterConnection。
c3p0的提供了两种处理空闲连接的机制,对应的配置参数分别是:idleConnectionTestPeriod和maxIdleTime。但是这两种机制在默认情况下对ReplicationConnection不奏效。原因如下:
默认情况下,c3p0使用DefaultConnectionTester(通过connectionTesterClassName配置)进行连接检查(基于Query),该类有以下两个比较重要的方法:
需要注意的是,MySQL connector也提供了一个实现了c3p0的ConnectionTester接口的类:MysqlConnectionTester。该类使用com.mysql.jdbc.Connection的ping方法(相对于执行query,ping更轻量级)来确认连接是否正常。笔者认为该类仍然不能正确检测ReplicationConnection。
以下是笔者实现的一个ConnectionTester,用于检查MySQL ReplicationConnection:
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mchange.v2.c3p0.AbstractConnectionTester; import com.mysql.jdbc.CommunicationsException; public final class MysqlReplicationConnectionTester extends AbstractConnectionTester { // private static final Logger LOGGER = LoggerFactory.getLogger(MysqlReplicationConnectionTester.class); // private static final long serialVersionUID = -7348778746126099053L; // private static final String DEFAULT_QUERY = "SELECT 1"; public boolean equals(Object o) { return (o != null && o.getClass() == MysqlReplicationConnectionTester.class); } public int hashCode() { return MysqlReplicationConnectionTester.class.getName().hashCode(); } public int activeCheckConnection(Connection c, String query, Throwable[] outParamCause) { // boolean readOnly = false; boolean needRestoreReadOnly = false; try { // readOnly = c.isReadOnly(); // int r = checkConnection(c, query, outParamCause, readOnly); if(r == CONNECTION_IS_OKAY) { needRestoreReadOnly = true; r = checkConnection(c, query, outParamCause, !readOnly); } return r; } catch(Exception e) { // LOGGER.warn("the connection: " + c + " was marked invalid", e); if (outParamCause != null) { outParamCause[0] = e; } return CONNECTION_IS_INVALID; } finally { try { if(needRestoreReadOnly) { c.setReadOnly(readOnly); } } catch (SQLException e) { LOGGER.error("failed to restore read only: " + readOnly + " on connection", e); } } } public int statusOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) { // int r = checkConnectionOnException(c, t, query, outParamCause); // if(r != CONNECTION_IS_OKAY) { if (outParamCause != null) { outParamCause[0] = t; } } return r; } private int checkConnection(Connection c, String query, Throwable[] outParamCause, Boolean readOnly) { // if (query == null || query.equals("")) { query = DEFAULT_QUERY; } // ResultSet rs = null; Statement stmt = null; try { // boolean ro = c.isReadOnly(); if(readOnly != null && readOnly != ro) { c.setReadOnly(readOnly); } // if(LOGGER.isInfoEnabled()) { LOGGER.info("testing connection: {} with query: {}, read only: {}", new Object[]{c, query, ro}); } // stmt = c.createStatement(); rs = stmt.executeQuery(query); return CONNECTION_IS_OKAY; } catch (SQLException e) { LOGGER.warn("failed to test connection: " + c + " with query: " + query + ", state: " + e.getSQLState(), e); if (outParamCause != null) { outParamCause[0] = e; } return CONNECTION_IS_INVALID; } catch (Exception e) { LOGGER.warn("failed to test connection: " + c + " with query: " + query, e); if (outParamCause != null) { outParamCause[0] = e; } return CONNECTION_IS_INVALID; } finally { closeQuietly(rs); closeQuietly(stmt); } } private int checkConnectionOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) { // if (t instanceof CommunicationsException) { return CONNECTION_IS_INVALID; } // if (t instanceof SQLException) { final String sqlState = ((SQLException) t).getSQLState(); if (sqlState != null && sqlState.startsWith("08")) { return CONNECTION_IS_INVALID; } else { return CONNECTION_IS_OKAY; } } // Runtime/Unchecked? return CONNECTION_IS_INVALID; } private void closeQuietly(ResultSet rs) { // if(rs == null) { return; } // try { rs.close(); } catch (SQLException e) { LOGGER.warn("failed to close result set", e); } } private void closeQuietly(Statement stat) { // if(stat == null) { return; } // try { stat.close(); } catch (SQLException e) { LOGGER.warn("failed to close statement", e); } } }
需要注意的是,以上代码适用于MySQL 5.0。对于MySQL 5.1,需要修改checkConnectionOnException方法,如下:
if (t instanceof CommunicationsException || "com.mysql.jdbc.exceptions.jdbc4.CommunicationsException".equals(throwable.getClass().getName())) { return CONNECTION_IS_INVALID; }
此外, 由于Spring Dao会转译SQLException,因此在Spring环境中,不能使用sqlState判断连接是否正常,而是需要使用基于query的方式,如下:
public int statusOnException(Connection c, Throwable t, String query, Throwable[] outParamCause) { return checkConnection(c, query, outParamCause, null); }