• 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中,处理逻辑如下:

    1. if (testWhileIdle) {
    2. final DruidConnectionHolder holder = poolableConnection.holder;
    3. long currentTimeMillis = System.currentTimeMillis();
    4. long lastActiveTimeMillis = holder.lastActiveTimeMillis;
    5. long lastExecTimeMillis = holder.lastExecTimeMillis;
    6. long lastKeepTimeMillis = holder.lastKeepTimeMillis;
    7. if (checkExecuteTime
    8. && lastExecTimeMillis != lastActiveTimeMillis) {
    9. lastActiveTimeMillis = lastExecTimeMillis;
    10. }
    11. if (lastKeepTimeMillis > lastActiveTimeMillis) {
    12. lastActiveTimeMillis = lastKeepTimeMillis;
    13. }
    14. long idleMillis = currentTimeMillis - lastActiveTimeMillis;
    15. long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
    16. if (timeBetweenEvictionRunsMillis <= 0) {
    17. timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
    18. }
    19. if (idleMillis >= timeBetweenEvictionRunsMillis
    20. || idleMillis < 0 // unexcepted branch
    21. ) {
    22. boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
    23. if (!validate) {
    24. if (LOG.isDebugEnabled()) {
    25. LOG.debug("skip not validate connection.");
    26. }
    27. discardConnection(poolableConnection.holder);
    28. continue;
    29. }
    30. }
    31. }

    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,关键代码如下:

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

    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,其中核心逻辑如下:

    1. public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
    2. if (conn.isClosed()) {
    3. return false;
    4. }
    5. if (usePingMethod) {
    6. if (conn instanceof DruidPooledConnection) {
    7. conn = ((DruidPooledConnection) conn).getConnection();
    8. }
    9. if (conn instanceof ConnectionProxy) {
    10. conn = ((ConnectionProxy) conn).getRawObject();
    11. }
    12. if (clazz.isAssignableFrom(conn.getClass())) {
    13. if (validationQueryTimeout <= 0) {
    14. validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
    15. }
    16. try {
    17. ping.invoke(conn, true, validationQueryTimeout * 1000);
    18. } catch (InvocationTargetException e) {
    19. Throwable cause = e.getCause();
    20. if (cause instanceof SQLException) {
    21. throw (SQLException) cause;
    22. }
    23. throw e;
    24. }
    25. return true;
    26. }
    27. }
    28. String query = validateQuery;
    29. if (validateQuery == null || validateQuery.isEmpty()) {
    30. query = DEFAULT_VALIDATION_QUERY;
    31. }
    32. Statement stmt = null;
    33. ResultSet rs = null;
    34. try {
    35. stmt = conn.createStatement();
    36. if (validationQueryTimeout > 0) {
    37. stmt.setQueryTimeout(validationQueryTimeout);
    38. }
    39. rs = stmt.executeQuery(query);
    40. return true;
    41. } finally {
    42. JdbcUtils.close(rs);
    43. JdbcUtils.close(stmt);
    44. }
    45. }

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

    1. public MySqlValidConnectionChecker(){
    2. try {
    3. clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
    4. if (clazz == null) {
    5. clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
    6. }
    7. if (clazz != null) {
    8. ping = clazz.getMethod("pingInternal", boolean.class, int.class);
    9. }
    10. if (ping != null) {
    11. usePingMethod = true;
    12. }
    13. } catch (Exception e) {
    14. LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e);
    15. }
    16. configFromProperties(System.getProperties());
    17. }
    18. @Override
    19. public void configFromProperties(Properties properties) {
    20. String property = properties.getProperty("druid.mysql.usePingMethod");
    21. if ("true".equals(property)) {
    22. setUsePingMethod(true);
    23. } else if ("false".equals(property)) {
    24. setUsePingMethod(false);
    25. }
    26. }

    同时,可以看出
    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 数据源中加上对应配置即可。拓展 如下:

    1. public class MySqlReplicationCompatibleValidConnectionChecker extends MySqlValidConnectionChecker {
    2. private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class);
    3. /**
    4. *
    5. */
    6. private static final long serialVersionUID = 1L;
    7. @Override
    8. public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
    9. if (conn.isClosed()) {
    10. return false;
    11. }
    12. if (conn instanceof DruidPooledConnection) {
    13. conn = ((DruidPooledConnection) conn).getConnection();
    14. }
    15. if (conn instanceof ConnectionProxy) {
    16. conn = ((ConnectionProxy) conn).getRawObject();
    17. }
    18. if (conn instanceof ReplicationConnection) {
    19. try {
    20. ((ReplicationConnection) conn).ping();
    21. LOG.info("validate connection success: connection=" + conn.toString());
    22. return true;
    23. } catch (SQLException e) {
    24. LOG.error("validate connection error: connection=" + conn.toString(), e);
    25. throw e;
    26. }
    27. }
    28. return super.isValidConnection(conn, validateQuery, validationQueryTimeout);
    29. }
    30. }


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

  • 相关阅读:
    模型选择、过拟合与欠拟合(多层感知机)
    数据库操作查看用户名和端口,以及如何Mac 版本idea 如何实现JDBC和MySql建立连接,以及如何操作数据以及连接时出现的常见错误
    MySQL日志
    Java面试题之并发
    【postgresql 基础入门】创建数据库的方法,存储位置,决定自己的数据的访问用户和范围
    选择适合的防火墙需要考虑哪些因素?
    小白学java
    LabVIEW性能和内存管理 8
    < Python全景系列-5 > 解锁Python并发编程:多线程和多进程的神秘面纱揭晓
    MySQL的
  • 原文地址:https://blog.csdn.net/java_beautiful/article/details/125443738