问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

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

发布网友 发布时间:2024-09-29 11:38

我来回答

1个回答

热心网友 时间:2024-09-29 15:47

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());}@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属性设置超时时间。

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

原文:https://juejin.cn/post/7112281560950243364
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
这是真的黑米还是假的? 牡丹江市区好玩的地方 显卡问题,现在的显卡显存一般都为多少? 现在主流显存是多少?512M5年后会过时吗 主流家用电脑配置家用电脑买什么样配置比较好 现在的电脑显存一般是多少? 安顺万家领秀城怎么样?好不好?值不值得买? 请问下面一道题怎么加标点 花园里 牡丹 月季 玫瑰 芍药 开得美丽极了... 是什么把大地打扮的这么漂亮呢 是雪呀 这两句话怎么写标点符号? 通州有那些私立高中啊 安阳市师专1988年进修毕业毕业证安阳承认嘛 蓝屏代码:STOP:0x0000007e C0xc0000005 OXF7660750 0XF78EB42C0XF78... 电脑蓝屏。错误代码STOP:0X0000007E (0xC0000005,0x8052BB80,0XF78E... 蓝屏0x0000007e(0xc0000005,0xf65846d,0xf78eea74,0xf78ee770) 蓝屏代码0X0000007E(0XC0000005,0XB970A081,0XF78EA340,0XF78EA03C... 0x0000007e(0xc0000005,0x805ef243,0xf78a9fc4,0xf78a9cc0,蓝屏怎么弄... 肠胃不好的人早饭怎么吃比较好? 建行有一种业务是每年存1万,每年返一千,连着存10年。最后再给2万利息... 直肠癌手术后怎么治疗?能不能治愈? 婆婆是直肠肿瘤的情况,已经是做了手术后的治疗,但还是有疼痛的情况... 直肠肿瘤良性手术后还用化疗吗 老妈今年54岁患了直肠管状腺恶性肿瘤,现已经手术切除肿瘤及周围淋巴,并... 直肠癌切除手术后,是否需要做化放疗?治疗过程中使用了核糖核酸(抗肠癌... 黑藻为什么线粒体,叶绿体都有?是不是既可以光合作用又可以有氧呼吸? 康佳电视机u盘可以放音乐不能看电影求解答 为什么用U盘下载的歌曲,放进去不能播放。 ...和视频,插上USB接口,只能播放音乐,不能播放视频,求解?【车型:途胜... 女生会不会喜欢人 百雀羚八杯水面膜可以每天敷吗 献血无偿,输血收费,钱让谁赚了? MySQL 大对象(BLOB)和字符串的分身术 MySQL不支持区域划分mysql不在某个区域 外国人如何到中国留学 电信ADSL宽带2兆的上行速率应该是多少?或者说它们是怎样规定的?_百度... 电信2兆是不是200多左右KB为什么我打电话里面的人说2兆是135以上KB? 支付宝6月7日的题目答案分享_支付宝6月7日的第二题的答案是什么 除了臭豆腐——(闻起来臭,吃起来香),还有什么 打床头柜是柜子剩的木头还是柜门 飘窗上可以放柜子吗 什么牌子的床头柜质量好?质量好的床头柜排行榜10强 ...看我,可是,等我转头看她她会立刻扭过头,持续了有一个月,为什么... 人在车上堵消防通道违法吗? 早上起来腿疼是怎么回事 有时候早上起来腿部特疼 厉害的时候上一点劲也没有,小腿肌肉疼也带着... ...4个多月了,现在长的很细也很稀,而且头发现在掉的也蛮多。有什么好... 斑秃快两个月了,头发秃了好几块,秃的地方有很细小的毛但好久都没长长... 1.头发稀少 细而且毛躁 2.以前头发掉的不是很厉害,就是最近几个月开始... 个人无常献血、病人用血、医院收费吗?要是医院收病人的费用。个人无常献... 无偿献血之献血者优惠,医院盈利与病人付费 绍兴越惠保既往症能理赔吗