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

作者:转转技术团队 时间:2024-01-19 11:04:26 

引言

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)。 显而易见,基于如上配置,按照常规理解,不应该出现&ldquo;The last packet successfully received from server was xxx,xxx,xxx milliseconds ago&rdquo;的问题。(当然,当时也排除了人工介入kill掉数据库连接的可能)。

当&ldquo;理所应当&rdquo;的经验解释不了问题所在,往往需要跳出可能浮于表面经验束缚,来一次追根究底。那么,该问题的真正原因是什么呢?

本质原因

当使用druid管理数据源,结合mysql-jdbc中原生的ReplicationConnection做读写分离时,ReplicationConnection代理对象中实际存在master和slaves两套连接,druid在做连接检测时候,只能检测到其中的master连接,如果某个slave连接长时间未使用,会导致连接失效问题。

原因分析

mysql-jdbc中,数据库驱动对连接的处理过程

结合com.mysql.jdbc.Driver源码,不难看出mysql-jdbc中获取连接的主体流程如下:

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

对于以&ldquo;jdbc:mysql:replication://&rdquo;开头配置的jdbc-url,通过mysql-jdbc获取到的连接,其实是一个ReplicationConnection的代理对象,默认情况下,&ldquo;jdbc:mysql:replication://&rdquo;后的第一个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做读写分离时,只要做一个&ldquo;设置connection的readOnly属性的&rdquo;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版),在未设置&ldquo;druid.mysql.usePingMethod&rdquo;系统属性的情况下,默认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连接。

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

题外一问:ping方法为什么使用&ldquo;pingInternal&rdquo;,而不是常规的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属性设置超时时间。

来源:https://juejin.cn/post/7112281560950243364

标签:MySQL,Replication,Connection,连接失效
0
投稿

猜你喜欢

  • Django后台获取前端post上传的文件方法

    2023-04-11 10:57:26
  • Php多进程实现代码

    2023-10-11 09:23:14
  • asp关键词屏蔽过滤函数代码

    2010-05-04 16:32:00
  • Python实现统计文章阅读量的方法详解

    2023-11-02 23:28:46
  • 如何利用python将Xmind用例转为Excel用例

    2022-06-18 19:18:46
  • Python求一批字符串的最长公共前缀算法示例

    2021-03-10 13:40:37
  • Python连接mysql数据库及简单增删改查操作示例代码

    2022-03-10 18:01:57
  • 使用Python-pptx 告别繁琐的幻灯片制作

    2021-12-10 12:03:23
  • python获取服务器响应cookie的实例

    2023-06-14 15:02:50
  • vue2.0 自定义 饼状图 (Echarts)组件的方法

    2024-04-27 15:51:07
  • Pandas 时间序列分析中的resample函数

    2023-12-06 16:38:33
  • 用Dreamweaver MX轻松操作表格

    2009-05-29 18:41:00
  • Python的类实例属性访问规则探讨

    2023-04-07 08:09:17
  • Python Sqlalchemy如何实现select for update

    2022-01-23 02:26:31
  • 理解生产者消费者模型及在Python编程中的运用实例

    2021-04-07 01:01:59
  • OpenCV中Canny边缘检测的实现

    2022-10-17 10:10:19
  • golang time包的用法详解

    2024-04-25 13:19:19
  • python numpy和list查询其中某个数的个数及定位方法

    2021-04-29 01:36:50
  • JS的千分位算法实现思路

    2023-08-23 22:40:32
  • django foreignkey外键使用的例子 相当于left join

    2021-04-17 15:52:33
  • asp之家 网络编程 m.aspxhome.com