【已解决】排查MySQL Communications link failure问题原因及源码剖析
一、现象
订单关联数据变更监控任务(每天凌晨 3 点执行),部署到测试环境后第一次跑就报错:
16:57:06.050 [qu_Worker-2] ERROR c.a.d.p.DruidDataSource [handleFatalError,1921] - {conn-10001} discard
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet successfully received from the server was 10,007 milliseconds ago. The last packet sent successfully to the server was 10,016 milliseconds ago.
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916)
at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:354)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3446)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:434)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3444)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:434)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3444)
at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:152)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:483)
at com.sun.proxy.$Proxy138.insert(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:59)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
at com.sun.proxy.$Proxy680.upsertChangedOrders(Unknown Source)
at com.bluebull.system.service.impl.OrderChangeRecordServiceImpl.checkAndRecordChange(OrderChangeRecordServiceImpl.java:32)
at com.bluebull.quartz.task.OrderChangeMonitorTask.monitorOrderChange(OrderChangeMonitorTask.java:37)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
连接才空闲 10 秒 就断开,并非经典的 “8 小时闲置被 MySQL 断开”。
二、关键线索:10 秒 vs 8 小时
| 因素 | 是否吻合 10 秒断开 |
|---|---|
MySQL wait_timeout=28800 |
❌ 8 小时,对不上 |
| OS TCP KeepAlive 默认 2 小时 | ❌ 来不及防护 |
| 网络中间设备(SLB / NAT / 防火墙)空闲超时 | ✅ 通常 10–60 秒 |
JDBC 驱动 socketTimeout 默认 0 = 无超时 |
✅ 驱动不主动感知断连,沿用 OS 行为 |
排查方向:中间设备先断 + 驱动不感知 = 任务跑起来才暴露。
三、根因(源码依据)
验证版本:
com.mysql:mysql-connector-j:8.0.33
3.1 默认值就是 0(不限超时)
com/mysql/cj/conf/PropertyDefinitions.java:
288: new IntegerPropertyDefinition(PropertyKey.connectTimeout, 0, RUNTIME_MODIFIABLE, ...)
306: new IntegerPropertyDefinition(PropertyKey.socketTimeout, 0, RUNTIME_MODIFIABLE, ...)
| 参数 | 默认 | 单位 | 范围 |
|---|---|---|---|
connectTimeout |
0 永不超时 |
毫秒 | [0, Integer.MAX_VALUE] |
socketTimeout |
0 永不超时 |
毫秒 | [0, Integer.MAX_VALUE] |
⚠️ “0 = 无超时”——不显式配置等于把超时交给 OS TCP 默认行为(Linux 由 tcp_syn_retries 决定,约 75~127 秒)。
3.2 参数确实是 JDBC URL 参数(大小写敏感)
com/mysql/cj/conf/PropertyKey.java:
101: connectTimeout("connectTimeout", true), // ← 第二个 true = 大小写敏感
218: socketTimeout ("socketTimeout", true), //
SocketTimeout=... 这种写法会被识别为 unknown property 并静默失效。
3.3 实际调用点
connectTimeout → com/mysql/cj/protocol/StandardSocketFactory.java:
127: int connectTimeout = pset.getIntegerProperty(PropertyKey.connectTimeout).getValue();
153: this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout));
最终落到 JDK Socket#connect(SocketAddress, int),控制 TCP 三次握手阶段超时。
socketTimeout → com/mysql/cj/protocol/a/NativeSocketConnection.java:
65: int socketTimeout = propSet.getIntegerProperty(PropertyKey.socketTimeout).getValue();
66: if (socketTimeout != 0) {
68: this.mysqlSocket.setSoTimeout(socketTimeout);
最终落到 JDK Socket#setSoTimeout(int),控制单次 read() 阻塞超时。 NativeProtocol#executeCommand() 在执行 query 时还会被 Statement#setQueryTimeout 临时覆盖,结束后恢复。
3.4 调用链总览
JDBC URL "?connectTimeout=3000&socketTimeout=30000"
↓ ConnectionUrlParser 解析
PropertyKey 枚举(大小写敏感字符串)
↓ PropertyDefinitions 注册(默认 0、范围、CATEGORY_NETWORK)
RuntimePropertyImpl.getValue()
├─► StandardSocketFactory#connect() → Socket.connect(addr, connectTimeout)
└─► NativeSocketConnection#connect() → Socket.setSoTimeout(socketTimeout)
NativeProtocol#executeCommand() → Socket.setSoTimeout(queryTimeout) / 恢复
结论:不配置 = 驱动永远等下去。中间设备 10 秒断开 → 死连接残留在池中 → 下次借出执行 SQL 才报 Communications link failure。
四、解决方案
JDBC URL 关键三参数:
url: jdbc:mysql://<host>:<port>/<dbname>?
useUnicode=true&characterEncoding=utf8&
zeroDateTimeBehavior=convertToNull&useSSL=true&
serverTimezone=GMT%2B8&
connectTimeout=30000& # 🔴 建连超时(30s)
socketTimeout=30000& # 🔴 读写超时(30s)
tcpKeepAlive=true # 互补:OS 层心跳保活
| 参数 | 作用 | 必要性 |
|---|---|---|
connectTimeout=30000 |
TCP 握手最长 30s,防止建连卡死 | 🔴 必配 |
socketTimeout=30000 |
单次 I/O 最长 30s,让驱动快速感知死连接 | 🔴 必配 |
tcpKeepAlive=true |
OS 周期发心跳包,对中间设备保活而非感知 | ⚠️ 互补 |
三者关系:
socketTimeout让驱动 fail-fast,tcpKeepAlive让中间设备别断;不是二选一。
五、实践效果
| 阶段 | URL 配置 | 凌晨任务结果 |
|---|---|---|
| 修复前 | 仅 tcpKeepAlive=true,无 timeout 参数 |
❌ 每次必报 CommunicationsException(10s 断连后下次借出立刻爆) |
| 修复后 | 增加 connectTimeout=30000&socketTimeout=30000 |
✅ 任务连续稳定运行(驱动可在 30s 内识别死连接并由连接池补连) |
事实依据:socketTimeout=30000 配置后,setSoTimeout(30000) 通过 NativeSocketConnection.java:68 生效,下次 read() 阻塞超 30s 即抛 SocketTimeoutException,连接被驱动判定失效并关闭,连接池补连新建可用连接。
六、各层超时职责分工
| 层 | 参数 | 控制什么 |
|---|---|---|
| 连接池(Druid / HikariCP) | maxWait / connectionTimeout |
从池中获取连接的最大等待(应用层) |
| JDBC 驱动 | connectTimeout |
建立新 TCP 连接的最大等待(驱动层) |
| JDBC 驱动 | socketTimeout |
单次 I/O 的最大等待(套接字层) |
| MySQL 服务端 | wait_timeout / interactive_timeout |
服务端空闲断开(DB 端) |
| OS | tcp_keepalive_* |
TCP 层活性探测(OS 内核) |
调优原则:socketTimeout 必须大于业务最慢 SQL 的 p99 × 2,否则慢查询会被驱动判超时,反而放大故障面。
七、Druid 双保险(推荐补充)
druid:
testOnBorrow: true # 借出时 SELECT 1 验证(防 testWhileIdle 检测窗口期)
testWhileIdle: true
timeBetweenEvictionRunsMillis: 10000 # 检测周期 10s,匹配中间设备超时
代价:每次借出多一次 SELECT 1(约 1ms 损耗),凌晨任务完全可接受。
八、最终推荐配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
initialSize: 5
minIdle: 10
maxActive: 20
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: true
timeBetweenEvictionRunsMillis: 10000
url: jdbc:mysql://<host>:<port>/<dbname>?
useUnicode=true&characterEncoding=utf8&
zeroDateTimeBehavior=convertToNull&useSSL=true&
serverTimezone=GMT%2B8&
tcpKeepAlive=true&
connectTimeout=30000&socketTimeout=30000
九、经验总结
socketTimeout不是"超时控制",是"死连接探测器"——默认 0 = 驱动永不感知断连。- 参数大小写敏感:
SocketTimeout≠socketTimeout,建议从常量类引用避免笔误。 - MySQL 8.0.13+ 驱动才支持
tcpKeepAlive,老驱动需升级。 - 看清错误中的"空闲多久"(10s vs 8h)能快速分流定位:
- 小时级 → 经典
wait_timeout问题; - 秒级 → 中间设备 + 驱动 socketTimeout 缺失。
- 小时级 → 经典
testOnBorrow=true是双保险,对凌晨低频任务零成本。
一句话:远程 MySQL(尤其经过 SLB/NAT)JDBC URL 必加 connectTimeout + socketTimeout,源码实证默认 0=永不超时,不配等于裸奔。
附录:源码版本
源码 jar:mysql-connector-j-8.0.33-sources.jar
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)