MySQL使用ReplicationConnection导致连接失效解决


引言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的同时 , 还充当对连接管理的角色 , 核心逻辑如下图:
【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 'select 1' instead.", e);}configfromproperties(system.getproperties());}@overridepublic 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;@overridepublic 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其它相关文章!

    推荐阅读