引言
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导致连接失效怎么解决的详细内容。更多信息请关注PHP中文网其他相关文章!

MySQL使用的是GPL许可证。1)GPL许可证允许自由使用、修改和分发MySQL,但修改后的分发需遵循GPL。2)商业许可证可避免公开修改,适合需要保密的商业应用。

选择InnoDB而不是MyISAM的情况包括:1)需要事务支持,2)高并发环境,3)需要高数据一致性;反之,选择MyISAM的情况包括:1)主要是读操作,2)不需要事务支持。InnoDB适合需要高数据一致性和事务处理的应用,如电商平台,而MyISAM适合读密集型且无需事务的应用,如博客系统。

在MySQL中,外键的作用是建立表与表之间的关系,确保数据的一致性和完整性。外键通过引用完整性检查和级联操作维护数据的有效性,使用时需注意性能优化和避免常见错误。

MySQL中有四种主要的索引类型:B-Tree索引、哈希索引、全文索引和空间索引。1.B-Tree索引适用于范围查询、排序和分组,适合在employees表的name列上创建。2.哈希索引适用于等值查询,适合在MEMORY存储引擎的hash_table表的id列上创建。3.全文索引用于文本搜索,适合在articles表的content列上创建。4.空间索引用于地理空间查询,适合在locations表的geom列上创建。

toCreateAnIndexinMysql,usethecReateIndexStatement.1)forasingLecolumn,使用“ createIndexIdx_lastNameEnemployees(lastName); 2)foracompositeIndex,使用“ createIndexIndexIndexIndexIndexDx_nameOmplayees(lastName,firstName,firstName);” 3)forauniqe instex,creationexexexexex,

MySQL和SQLite的主要区别在于设计理念和使用场景:1.MySQL适用于大型应用和企业级解决方案,支持高性能和高并发;2.SQLite适合移动应用和桌面软件,轻量级且易于嵌入。

MySQL中的索引是数据库表中一列或多列的有序结构,用于加速数据检索。1)索引通过减少扫描数据量提升查询速度。2)B-Tree索引利用平衡树结构,适合范围查询和排序。3)创建索引使用CREATEINDEX语句,如CREATEINDEXidx_customer_idONorders(customer_id)。4)复合索引可优化多列查询,如CREATEINDEXidx_customer_orderONorders(customer_id,order_date)。5)使用EXPLAIN分析查询计划,避

在MySQL中使用事务可以确保数据一致性。1)通过STARTTRANSACTION开始事务,执行SQL操作后用COMMIT提交或ROLLBACK回滚。2)使用SAVEPOINT可以设置保存点,允许部分回滚。3)性能优化建议包括缩短事务时间、避免大规模查询和合理使用隔离级别。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

SecLists
SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 Linux新版
SublimeText3 Linux最新版

DVWA
Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

ZendStudio 13.5.1 Mac
功能强大的PHP集成开发环境

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。