MySQL使用ReplicationConnection导致的连接失效分析与解决

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中 , 处理逻辑如下:
【MySQL使用ReplicationConnection导致的连接失效分析与解决】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); }


推荐阅读