目前项目中使用了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:
- 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);
- }