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的同时,还充当对连接管理的角色,核心逻辑如下图:

对于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连接失效的资料请关注我们其它相关文章!

(0)

相关推荐

  • MySQL 5.7增强版Semisync Replication性能优化

    一 前言 前文 介绍了5.5/5.6 版本的MySQL semi sync 基础原理和配置,随着MySQL 5.7 的发布,新版本的MySQL修复了semi sync 的一些bug 并且增强了功能. 支持发送binlog和接受ack的异步化; 支持在事务commit前等待ACK; 在server层判断备库是否要求半同步以减少Plugin锁冲突; 解除binlog dump线程和lock_log的冲突等等. 本文重点分析 第1,2个改进项,因为原来的模式的确会影响系统的tps,新的异步模式可以提高

  • MySQL5.6 Replication主从复制(读写分离) 配置完整版

    MySQL5.6主从复制(读写分离)教程 1.MySQL5.6开始主从复制有两种方式: 基于日志(binlog): 基于GTID(全局事务标示符). 需要注意的是:GTID方式不支持临时表!所以如果你的业务系统要用到临时表的话就不要考虑这种方式了,至少目前最新版本MySQL5.6.12的GTID复制还是不支持临时表的. 所以本教程主要是告诉大家如何通过日志(binlog)方式做主从复制! 2.MySQL官方提供的MySQL Replication教程: http://dev.mysql.com/

  • 解决JDBC连接Mysql长时间无动作连接失效的问题

    错误场景介绍 做的有一个项目使用JDBC手动创建Connection实现了一个简单的自定义数据库连接池,用来支持Canal解析数据库Binlog指定业务库的插入修改SQL来进行数据库分表备份(按照月份)操作. 但是发现当一个一段时间(较长)没有进行数据库操作时,连接都失效了,导致SQL执行失败失效提示为No operations allowed after connection closed 查明原因 经过搜索发现这个问题是由于Mysql默认一个已创建的长连接28800秒(八小时)内没有任何动作

  • MySQL-group-replication 配置步骤(推荐)

    MySQL-Group-Replication 是mysql-5.7.17版本开发出来的新特性:它在master-slave 之间实现了强一致性, 但是就目前来说主要是性能不太好. [1]确定当前的mysql数据库版本为5.7.17及以上 /usr/local/mysql/bin/mysqld --version /usr/local/mysql/bin/mysqld Ver 5.7.17 for linux-glibc2.5 on x86_64 (MySQL Community Server

  • Mysql错误:Too many connections的解决方法

    MySQL数据库 Too many connections 出现这种错误明显就是 mysql_connect 之后忘记 mysql_close: 当大量的connect之后,就会出现Too many connections的错误,mysql默认的连接为100个,而什么情况下会出现这种错误呢? 正常的mysql_connect 之后调用 mysql_close()关闭连接 但在连接错误时,会者mysql_real_query()出现错误退出时,可能忘记mysql_close(); 所以在程序ret

  • MySQL too many connections错误的原因及解决

    今天中午,开发测试环境的MySQL服务报了一个too many connections的错误,从问题上看,可能是连接池被打满了,导致所有的连接都不可用了. 在这种情况下,最为直接的办法就是重新设置最大连接数,查看my.cnf文件,里面关于连接数的参数有两个,分别是: max_connections:最大连接数 max_user_connections:用户最大连接数 其中,第一个参数确定的是该实例的最大连接数,第二个参数确定的是单个用户的最大连接数. 一般的线上环境,为了保险起见,一般这两个参数

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

    引言 MySQL数据库读写分离,是提高服务质量的常用手段之一,而对于技术方案,有很多成熟开源框架或方案,例如:sharding-jdbc.spring中的AbstractRoutingDatasource.MySQL-Router等,而mysql-jdbc中的ReplicationConnection亦可支持. 本文暂不对读写分离的技术选型做过多的分析,只是探索在使用druid作为数据源.结合ReplicationConnection做读写分离时,连接失效的原因,并找到一个简单有效的解决方案.

  • mysql密码过期导致连接不上mysql

    mysql密码过期了,今天遇到了连接mysql,总是连接不上去, 错误现象1: An attempt by a client to checkout a Connection has timed out 第一次出现连接超时错误,第一反应是去修改cpool.checkoutTimeout参数为18000,初始为500,结果还是错误, 在修改为36000,并且加大了c3p0连接数cpool.maxPoolSize=100 这个时候出现下面错误现象2 Connections could not be

  • MySQL隐式类型转换导致索引失效的解决

    目录 问题 复现 隐式转换 总结 参考 问题 在工作中发现,有一个接口只执行一条SQL查询语句,并且SQL明明使用了主键列,但是速度很慢. 在MySQL中EXPLAINN后发现,执行时并没有使用主键索引,而是进行了全表扫描. 复现 数据表DDL如下,使用 user_id 作为主键索引: CREATE TABLE `user_message` ( `user_id` varchar(50) NOT NULL COMMENT '用户ID', `msg_id` int(11) NOT NULL COM

  • 浅谈mysql哪些情况会导致索引失效

    下面有一些培训教学机构的口诀和我个人的一些总结: 为了讲解以下索引内容,我们先建立一个临时的表 test02 CREATE TABLE `sys_user` ( `id` varchar(64) NOT NULL COMMENT '主键', `name` varchar(64) DEFAULT NULL COMMENT '名字', `age` int(64) DEFAULT NULL COMMENT '年龄', `pos` varchar(64) DEFAULT NULL COMMENT '职位

  • 完美解决mysql客户端授权后连接失败的问题

    在本地(192.168.1.152)部署好mysql环境,授权远程客户机192.168.1.%连接本机的mysql,在iptables防火墙也已开通3306端口. 如下: mysql> select host,user,password from mysql.user; +--------------+-----------------+---------------------------------------------------------+ | host | user | passw

  • gearman中worker常驻后台,导致MySQL server has gone away的解决方法

    本文实例讲述了gearman中worker常驻后台,导致MySQL server has gone away的解决方法.分享给大家供大家参考,具体如下: 产生这个原因主要有如下几点: 1.mysql服务宕机了 2.长时间没有操作,超过了wait_timeout的设置,mysql自动断开 3.mysql请求链接被主动kill 4.发送的请求或返回结果过大,可设置max_allowed_packet的值 5.程序中你都是通过单例来操作数据库,如果两个操作时间超过wait_timeout. 为了演示的

  • MySQL使用innobackupex备份连接服务器失败的解决方法

    MySQL使用innobackupex备份连接服务器失败,怎么办? 使用 innobackupex  备份时有错误信息如下: Failed to connect to MySQL server as DBD::mysql module is not installed at - line 1327. 原因: 未安装基于 perl 的 mysql 接口工具 perl-DBD-MySQL 解决方法: # perl-DBD-MySQL 是否安装 # yum list installed | grep

  • Xcode 升级导致插件失效的解决办法

    最近升级Xcode8 遇到了些问题,这里整理下,希望读者少走弯路,尽快解决问题. Xcode8不能使用插件了,此文作废... Xcode的各种插件极大地提高了开发效率,比如我常用的FuzzyAutocomplete(添加模糊匹配来提高Xcode代码自动补全功能,开发者无需遵循从头匹配的原则,只要记得方法里某个关键字即可进行匹配). VVDocument.KSImageNamed.Peckham(可以在任意位置通过快捷键添加#import 语句).XAlign(代码对齐),但是升级Xcode导致插

  • navicat连接mysql时出现1045错误的解决方法

    navicat for MySQL 连接本地数据库出现1045错误 如下图: 查了很多资料,意思是说mysql没有授权远程连接,也就是权限不够: 解决方法: 1.首先打开命令行:开始->运行->cmd. 2.先进入电脑安装的mysql的bin目录下,因为我用的是phpstudy,mysql安装在D盘,如果你的是C盘 就不用执行第一个命令 D: cd D:\phpStudy\MySQL\bin mysql -u root mysql mysql> UPDATE user SET Passw

随机推荐