首頁  >  文章  >  資料庫  >  MySQL使用ReplicationConnection導致連線失效怎麼解決

MySQL使用ReplicationConnection導致連線失效怎麼解決

PHPz
PHPz轉載
2023-05-26 13:10:321428瀏覽

引言

MySQL資料庫讀寫分離,是提高服務品質的常用手段之一,而對於技術方案,有很多成熟開源框架或方案,例如:sharding-jdbc、spring中的AbstractRoutingDatasource、 MySQL-Router等,而mysql-jdbc中的ReplicationConnection也可支援。

本文暫不對讀寫分離的技術選型做過多的分析,只是探討在使用druid作為資料來源、結合ReplicationConnection做讀寫分離時,連線失效的原因,並找到一個簡單有效的解決方案。

問題背景

由於歷史原因,某幾個服務出現連線失效異常,關鍵報錯如下:

MySQL使用ReplicationConnection導致連線失效怎麼解決

##根據日誌可以推斷出,這是因為該連接長時間未與MySQL服務端進行交互,導致服務端關閉了連接,典型的連接失效情況。

涉及的主要設定

jdbc設定

#jdbc:mysql:replication://master_host:port,slave_host:port/database_name

druid配置

testWhileIdle=true(即,開啟了空閒連線檢查);

timeBetweenEvictionRunsMillis=6000L(即,對於取得連線的場景,如果某連線空閒時間超過1分鐘,將會進行檢查,如果連線無效,將拋棄後重新取得)。

附:DruidDataSource.getConnectionDirect中

處理邏輯如下:

if (testWhileIdle) {
    final DruidConnectionHolder holder = poolableConnection.holder;
    long currentTimeMillis             = System.currentTimeMillis();
    long lastActiveTimeMillis          = holder.lastActiveTimeMillis;
    long lastExecTimeMillis            = holder.lastExecTimeMillis;
    long lastKeepTimeMillis            = holder.lastKeepTimeMillis;
    if (checkExecuteTime
            && lastExecTimeMillis != lastActiveTimeMillis) {
        lastActiveTimeMillis = lastExecTimeMillis;
    }
    if (lastKeepTimeMillis > lastActiveTimeMillis) {
        lastActiveTimeMillis = lastKeepTimeMillis;
    }
    long idleMillis    = currentTimeMillis - lastActiveTimeMillis;
    long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
    if (timeBetweenEvictionRunsMillis <= 0) {
        timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
    }
    if (idleMillis >= timeBetweenEvictionRunsMillis
            || idleMillis < 0 // unexcepted branch
            ) {
        boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
        if (!validate) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("skip not validate connection.");
            }
            discardConnection(poolableConnection.holder);
             continue;
        }
    }
}

mysql逾時參數配置wait_timeout=3600(3600秒,即:如果某連接超過一個小時和服務端沒有交互,該連接將會被服務端kill)。顯而易見,基於如上配置,按照常規理解,不應該出現“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的問題。 (當然,當時也排除了人工介入kill掉資料庫連結的可能)。 當「理所當然」的經驗解釋不了問題所在,往往需要跳出可能浮於表面經驗束縛,來一次追根究底。那麼,該問題的真正原因是什麼呢?

本質原因

當使用druid管理資料來源,結合mysql-jdbc中原生的ReplicationConnection做讀寫分離時,ReplicationConnection代理物件中實際存在master和slaves兩套連接,druid在做連接偵測時候,只能偵測到其中的master連接,如果某個slave連線長時間未使用,會導致連線失效問題。

原因分析

mysql-jdbc中,資料庫驅動對連接的處理過程
結合com.mysql.jdbc.Driver源碼,不難看出mysql-jdbc中獲取連接的主體流程如下:

MySQL使用ReplicationConnection導致連線失效怎麼解決

對於以「jdbc:mysql:replication://」開頭配置的jdbc-url,透過mysql-jdbc取得的連接,其實是一個ReplicationConnection的代理對象,預設情況下,「jdbc:mysql:replication://」後的第一個host和port對應master連接,其後的host和port對應slaves連接,而對於存在多個slave配置的場景,預設使用隨機策略進行負載平衡。

ReplicationConnection代理對象,使用JDK動態代理產生的,其中InvocationHandler的具體實現,是ReplicationConnectionProxy,關鍵代碼如下:

public static ReplicationConnection createProxyInstance(List<String> masterHostList, Properties masterProperties, List<String> slaveHostList,
            Properties slaveProperties) throws SQLException {
      ReplicationConnectionProxy connProxy = new ReplicationConnectionProxy(masterHostList, masterProperties, slaveHostList, slaveProperties);
      return (ReplicationConnection) java.lang.reflect.Proxy.newProxyInstance(ReplicationConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy);
 }

ReplicationConnectionProxy的重要組成
關於資料庫連接代理,ReplicationConnectionProxy中的主要組成如下圖:

MySQL使用ReplicationConnection導致連線失效怎麼解決

#ReplicationConnectionProxy存在masterConnection和slavesConnection兩個實際連接對象,currentConnetion(當前連接)可以切換成mastetConnection或者slavesConnection,切換方式可以透過設定readOnly實現。

業務邏輯中,實作讀寫分離的核心也在於此,簡單來說:使用ReplicationConnection做讀寫分離時,只要做一個「設定connection的readOnly屬性的」aop即可。

基於ReplicationConnectionProxy,在業務邏輯中取得到的Connection代理對象,資料庫存取時的主要邏輯是什麼樣的呢?

ReplicationConnection代理對象處理過程
對於業務邏輯而言,取得到的Connection實例,是ReplicationConnection代理對象,該代理對象透過ReplicationConnectionProxy和ReplicationMySQLConnection相互協同完成對資料庫存取的處理,其中ReplicationConnectionProxy在實作InvocationHandler的同時,也扮演對連線管理的角色,核心邏輯如下圖:

MySQL使用ReplicationConnection導致連線失效怎麼解決

对于prepareStatement等常规逻辑,ConnectionMySQConnection获取到当前连接进行处理(普通的读写分离的处理的重点正是在此);此时,重点提及pingInternal方法,其处理方式也是获取当前连接,然后执行pingInternal逻辑。

对于ping()这个特殊逻辑,图中描述相对简单,但主体含义不变,即:对master连接和sleves连接都要进行ping()的处理。

图中,pingInternal流程和druid的MySQ连接检查有关,而ping的特殊处理,也正是解决问题的关键。

druid数据源对MySQ连接的检查

druid中对MySQL连接检查的默认实现类是MySqlValidConnectionChecker,其中核心逻辑如下:

public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
    if (conn.isClosed()) {
        return false;
    }
    if (usePingMethod) {
        if (conn instanceof DruidPooledConnection) {
            conn = ((DruidPooledConnection) conn).getConnection();
        }
        if (conn instanceof ConnectionProxy) {
            conn = ((ConnectionProxy) conn).getRawObject();
        }
        if (clazz.isAssignableFrom(conn.getClass())) {
            if (validationQueryTimeout <= 0) {
                validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
            }
            try {
                ping.invoke(conn, true, validationQueryTimeout * 1000);
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof SQLException) {
                    throw (SQLException) cause;
                }
                throw e;
            }
            return true;
        }
    }
    String query = validateQuery;
    if (validateQuery == null || validateQuery.isEmpty()) {
        query = DEFAULT_VALIDATION_QUERY;
    }
    Statement stmt = null;
    ResultSet rs = null;
    try {
        stmt = conn.createStatement();
        if (validationQueryTimeout > 0) {
            stmt.setQueryTimeout(validationQueryTimeout);
        }
        rs = stmt.executeQuery(query);
        return true;
    } finally {
        JdbcUtils.close(rs);
        JdbcUtils.close(stmt);
    }
}

对应服务中使用的mysql-jdbc(5.1.45版),在未设置“druid.mysql.usePingMethod”系统属性的情况下,默认usePingMethod为true,如下:

public MySqlValidConnectionChecker(){
try {
        clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
        if (clazz == null) {
            clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
        }
        if (clazz != null) {
            ping = clazz.getMethod("pingInternal", boolean.class, int.class);
        }
        if (ping != null) {
            usePingMethod = true;
        }
    } catch (Exception e) {
        LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method.  Will use &#39;SELECT 1&#39; instead.", e);
    }
    configFromProperties(System.getProperties());
}
@Override
public void configFromProperties(Properties properties) {
    String property = properties.getProperty("druid.mysql.usePingMethod");
    if ("true".equals(property)) {
        setUsePingMethod(true);
    } else if ("false".equals(property)) {
        setUsePingMethod(false);
    }
}

同时,可以看出MySqlValidConnectionChecker中的ping方法使用的是MySQLConnection中的pingInternal方法,而该方法,结合上面对ReplicationConnection的分析,当调用pingInternal时,只是对当前连接进行检验。执行检验连接的时机是通过DrduiDatasource获取连接时,此时未设置readOnly属性,检查的连接,其实只是ReplicationConnectionProxy中的master连接。

此外,如果通过“druid.mysql.usePingMethod”属性设置usePingMeghod为false,其实也会导致连接失效的问题,因为:当通过valideQuery(例如“select 1”)进行连接校验时,会走到ReplicationConnection中的普通查询逻辑,此时对应的连接依然是master连接。

题外一问:ping方法为什么使用“pingInternal”,而不是常规的ping?

原因:pingInternal预留了超时时间等控制参数。

解决方式

调整依赖版本

在服务中,使用的MySQL JDBC版本是5.1.45,并且使用的Druid版本是1.1.20。经过对其他高版本依赖的了解,依然存在该问题。

修改读写分离实现

修改的工作量主要在于数据源配置和aop调整,但需要一定的整体回归验证成本,鉴于涉及该问题的服务重要性一般,暂不做大调整。

拓展mysql-jdbc驱动

基于原有ReplicationConnection的功能,拓展pingInternal调整为普通的ping,集成原有Driver拓展新的Driver。方案可行,但修改成本不算小。

基于druid,拓展MySQL连接检查

为简单高效解决问题,选择拓展MySqlValidConnectionChecker,并在druid数据源中加上对应配置即可。拓展如下:

public class MySqlReplicationCompatibleValidConnectionChecker extends MySqlValidConnectionChecker {
    private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class);
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    @Override
    public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
        if (conn.isClosed()) {
            return false;
        }
        if (conn instanceof DruidPooledConnection) {
            conn = ((DruidPooledConnection) conn).getConnection();
        }
        if (conn instanceof ConnectionProxy) {
            conn = ((ConnectionProxy) conn).getRawObject();
        }
        if (conn instanceof ReplicationConnection) {
            try {
                ((ReplicationConnection) conn).ping();
                LOG.info("validate connection success: connection=" + conn.toString());
                return true;
            } catch (SQLException e) {
                LOG.error("validate connection error: connection=" + conn.toString(), e);
                throw e;
            }
        }
        return super.isValidConnection(conn, validateQuery, validationQueryTimeout);
    }
}

ReplicatoinConnection.ping()的实现逻辑中,会对所有master和slaves连接进行ping操作,最终每个ping操作都会调用到LoadBalancedConnectionProxy.doPing进行处理,而此处,可在数据库配置url中设置loadBalancePingTimeout属性设置超时时间。

以上是MySQL使用ReplicationConnection導致連線失效怎麼解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除