MySQL事务悬挂源于TCP静默断连后服务端未感知,需内核tcp_keepalive(如600s探测)、客户端连接池生命周期(如5分钟)及应用层超时三者协同防控,单点优化无效。

MySQL 事务悬挂不是 MySQL 自己“卡住”的,而是 TCP 连接无声断开后,服务端没及时感知,导致事务和锁一直挂着——必须靠 TCP Keepalive 主动探测来打破这种假连接状态。
为什么 wait_timeout 改小了还是挂?
因为 wait_timeout 只控制 MySQL 层面的会话空闲超时,它不等于 TCP 连接存活时间。当网络中间设备(如 NAT、防火墙、负载均衡)在 MySQL 超时前就清掉了连接,而 MySQL 没收到 FIN/RST 包,就会继续认为连接还活着,事务照常 hold 锁。
-
wait_timeout默认 28800 秒(8 小时),但实际生效受客户端是否带interactive标志影响,interactive_timeout才是交互式连接用的值 - MySQL 本身不发保活包,
tcp_keepalive是操作系统内核行为,MySQL 进程必须运行在已启用并调优过内核参数的系统上 - 如果
tcp_keepalive_time≥wait_timeout,那保活探测根本来不及触发,连接就被 MySQL 自己关了——起不到防悬挂作用
Linux 内核参数必须改,且要同步生效
在运行 mysqld 的服务器上执行:
echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl echo 3 > /proc/sys/net/ipv4/tcp_keepalive_probes
这组配置表示:连接空闲 10 分钟后开始探测,每 60 秒发一次,连续 3 次无响应则断连。
- 必须确保
tcp_keepalive_time显著小于wait_timeout(比如设为 600,wait_timeout设为 1800) - 仅修改
my.cnf中的tcp_keepalive_time(MySQL 5.7+ 支持)无效,那是误导项;MySQL 不管理底层 socket 的 keepalive 开关 - 用
ss -i查看活跃连接,输出中出现ka字样才说明内核保活已对这个 socket 生效
客户端连接池生命周期必须比保活周期更短
以 Go 的 database/sql 为例,如果只依赖内核保活,仍可能在保活探测间隙里拿到一个“半死”连接。
-
db.SetConnMaxLifetime(5 * time.Minute):强制连接在 5 分钟后重建,比内核tcp_keepalive_time=600更激进,可兜底 -
db.SetMaxIdleConns(10)和db.SetMaxOpenConns(25)防止连接堆积,避免大量“将死未死”的连接同时等待探测 - Java HikariCP 同理,
max-lifetime建议设为 300000(5 分钟),keepalive-time设为 30000(30 秒)主动心跳,两者配合
事务中网络异常时,应用层不能假设 rollback 自动发生
模拟 client 断电后,MySQL 侧 SHOW PROCESSLIST 仍显示 State: executing 或 State: updating,锁持续存在,新事务会被阻塞。
- 不要依赖 MySQL 自动回滚——它只在收到 RST 或连接关闭通知后才清理,而静默断连不会触发
- 应用必须设置 SQL 执行级超时(如 JDBC 的
socketTimeout或queryTimeout),并在 catch 到java.sql.SQLNonTransientConnectionException等网络错误时,明确放弃当前事务上下文 - 长事务尤其危险:哪怕保活生效,探测到断连也需要最多
tcp_keepalive_time + tcp_keepalive_intvl × probes(例如 600+60×3=780 秒)才能释放锁
真正起作用的是内核参数 + 客户端连接池生命周期 + 应用超时三者嵌套约束,单点优化基本没用。最容易被忽略的是:开发环境跑得好好的,上线后因生产环境中间设备策略不同,保活包被直接丢弃——得用 tcpdump 抓包确认保活包是否发出、是否被响应。



















