引言

mysql数据库读写分离,是提高服务质量的常用手段之一,而对于技术方案,有很多成熟开源框架或方案,例如:sharding-jdbc、spring中的abstractroutingdatasource、mysql-router等,而mysql-jdbc中的replicationconnection亦可支持。

本文暂不对读写分离的技术选型做过多的分析,只是探索在使用druid作为数据源、结合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中获取连接的主体流程如下:

对于以“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中的主要组成如下图:

replicationconnectionproxy存在masterconnection和slavesconnection两个实际连接对象,currentconnetion(当前连接)可以切换成mastetconnection或者slavesconnection,切换方式可以通过设置readonly实现。

业务逻辑中,实现读写分离的核心也在于此,简单来说:使用replicationconnection做读写分离时,只要做一个“设置connection的readonly属性的”aop即可。

基于replicationconnectionproxy,业务逻辑中获取到的connection代理对象,数据库访问时的主要逻辑是什么样的呢?

replicationconnection代理对象处理过程

对于业务逻辑而言,获取到的connection实例,是replicationconnection代理对象,该代理对象通过replicationconnectionproxy和replicationmysqlconnection相互协同完成对数据库访问的处理,其中replicationconnectionproxy在实现 invocationhandler的同时,还充当对连接管理的角色,核心逻辑如下图:

对于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 'select 1' 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导致连接失效解决的详细内容,更多关于mysql replication连接失效的资料请关注www.887551.com其它相关文章!