长事务拖垮数据库是因为持续占锁、阻塞会话、膨胀undo log,使MVCC快照无法清理;诱因包括未设超时、忘提交/回滚、事务内调用外部服务;需监控TRX_STARTED超5秒告警,并强制配对事务控制。

长事务为什么拖垮整个数据库
长事务不是“慢一点”,而是持续占用锁、阻塞其他会话、膨胀 undo log,最终让 SELECT 都变卡。MySQL 的 MVCC 机制依赖事务快照,一个运行了 10 分钟的事务,会让所有后续事务的快照无法清理旧版本,information_schema.INNODB_TRX 里能看到它卡在 TRX_STATE = 'RUNNING',但实际早就不干活了。
常见诱因是应用层没设超时、手动开启事务后忘记 COMMIT 或 ROLLBACK、或者在事务里混入了 HTTP 调用、文件读写等外部耗时操作。
- 监控必须盯住
TRX_STARTED时间戳,超过 5 秒就该告警 - 应用代码里所有
BEGIN后必须配对COMMIT/ROLLBACK,建议用 try-finally 或 ORM 的上下文管理器 - 禁止在事务内调用外部服务;真要调用,先
COMMIT,再请求,最后根据结果决定是否补新事务
死锁日志怎么看才不抓瞎
MySQL 的死锁信息藏在错误日志里,不是每次 Deadlock found when trying to get lock 都会打印完整链路——只有被选为 victim 的那个事务才会输出 SHOW ENGINE INNODB STATUS 片段。关键要看 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: 和 *** (2) HOLDS THE LOCK(S): 这两块。
典型陷阱是以为“谁先执行谁赢”,其实 InnoDB 按事务权重(undo log 大小 + 修改行数)选 victim,和执行顺序无关;另一个误区是只看 SQL 文本,忽略隐式锁(比如唯一索引冲突触发的插入意向锁)。
- 用
SELECT * FROM information_schema.INNODB_LOCK_WAITS实时查等待关系(注意:5.7+ 才有) - 死锁复现时,立刻执行
SHOW ENGINE INNODB STATUS\G,别等日志轮转 - 检查是否用了
SELECT ... FOR UPDATE却没走索引——全表扫描会锁整张表,极易引发连锁死锁
如何用最小代价定位长事务源头
靠人工翻应用日志效率太低。MySQL 自带线索足够用:从 performance_schema.events_statements_history_long 可反查长时间未结束的事务对应哪条原始 SQL,再结合 THREAD_ID 关联到 performance_schema.threads 查 PROCESSLIST_ID,就能定位是哪个连接、哪个应用实例发起的。
难点在于历史记录默认只保留 1 万条,且不包含事务开始时间。所以得提前开开关:SET GLOBAL performance_schema_events_statements_history_long_size = 100000;,并确保 events_statements_history_long 的消费者已启用。
- 定期跑这个查询:
SELECT t.PROCESSLIST_ID, t.PROCESSLIST_INFO, trx.trx_started FROM performance_schema.threads t JOIN information_schema.INNODB_TRX trx ON t.THREAD_ID = trx.trx_mysql_thread_id WHERE trx.trx_started - 如果看到大量
PROCESSLIST_INFO是NULL,说明 SQL 已被刷出 history 表,得调大缓冲区 - Java 应用建议在连接 URL 加
&useSSL=false&allowPublicKeyRetrieval=true&connectTimeout=3000&socketTimeout=30000,避免网络卡顿伪装成长事务
死锁能预防,但不能靠加索引一劳永逸
加索引确实能缩小锁范围,比如把 UPDATE user SET status=1 WHERE name='a' 从表锁变成行锁,但前提是 name 是唯一索引。如果是普通索引,InnoDB 会加间隙锁(gap lock),反而可能扩大锁定范围——两个事务同时 INSERT INTO user(name) VALUES('b'),哪怕表里没有 'b',也可能因间隙重叠而死锁。
更隐蔽的问题是“锁升级”:当更新行数超过阈值(默认 2000 行),InnoDB 可能放弃行锁,直接锁整个二级索引 B+ 树,这时候加索引反而加重竞争。
- 批量更新务必控制
IN子句长度,单次不超过 500 行,用循环分批 - 业务上能用
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE就别用SELECT + UPDATE,减少锁持有时间 - 测试阶段用
SELECT SLEEP(10)故意制造长事务,压测时观察死锁频率变化,比光看 explain 更真实
真正难处理的是跨库事务、分布式锁与 MySQL 本地锁的耦合,那已经不是加索引或调参数能解决的事了。



















