Undo Log膨胀与锁等待本质是同一根因——事务未及时提交;必须先定位并终止trx_state='RUNNING'且trx_query IS NULL的长事务,否则调参无效。

查长事务不能只看 SHOW PROCESSLIST 的 Time 字段
这个字段只记录当前语句执行时长,对已空闲但未提交的事务完全失真。真正卡住 purge 和锁资源的,是那些 trx_state = 'RUNNING' 且 trx_query IS NULL 的连接。
- 运行这条语句定位真凶:
SELECT trx_id, trx_started, TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS duration_sec, trx_state, trx_rows_modified, trx_query, trx_mysql_thread_id FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 600;
- 重点关注
duration_sec > 600且trx_query IS NULL的记录;trx_rows_modified > 0表示它确实在改数据,不是只读快照 - 用
trx_mysql_thread_id去关联information_schema.PROCESSLIST,查出HOST、USER、INFO,确认是不是某台 Python 服务崩溃后残留的连接
KILL 前必须分 trx_state 判断,乱杀会雪上加霜
不同状态对应完全不同的处理逻辑,错一步就可能让回滚变异步、IO 爆满、purge 停摆。
-
trx_state = 'RUNNING'且trx_query IS NULL:99% 是应用漏了COMMIT或连接池没 close,可安全KILL对应线程 -
trx_state = 'LOCK WAIT':它被别的事务堵住了,先查INNODB_LOCK_WAITS找出blocking_trx_id,优先干掉上游 -
trx_state = 'ROLLING BACK':千万别动!此时 KILL 会让回滚从同步变异步,耗时翻倍、IO 更爆,甚至拖垮整个 purge 线程 - 若
trx_rows_modified > 100000,回滚可能持续数分钟,KILL 前必须确认业务是否允许中断
批量更新百万行必须拆成主键区间 + 显式 COMMIT
单条 UPDATE 影响百万行,InnoDB 就得全程持有百万行锁、持续写 undo、阻塞 purge——这不是“优化”能绕开的,是硬性禁止。
- 放弃
LIMIT OFFSET(越往后越慢、还可能漏行),改用主键范围驱动:UPDATE t SET status = 1 WHERE id > @start_id AND id <= @start_id + 1000;
- 每批严格控制在 500–1000 行以内;单行越宽、批次越小;STATEMENT 格式 binlog 下更要保守
- 每次
UPDATE后立刻COMMIT,再用ROW_COUNT()判断是否继续 - 批次间加
DO SLEEP(0.05),缓解锁竞争和主从压力
应用层必须切断事务泄漏源头,ORM 不等于自动兜底
Spring @Transactional、MyBatis、Django ORM 都不会替你决定“什么时候该 COMMIT”。一个 HTTP 请求里开了事务,中间调个 HTTP 接口或 sleep(3),Undo Log 就开始堆积,锁等待就已发生。
- 禁止在事务内做任何 I/O:HTTP 调用、文件读写、消息队列
PUBLISH、日志刷盘都必须移出事务外 - 连接池配置
connection-timeout没用,MySQL 服务端必须设wait_timeout = 300(5 分钟),否则空闲事务一直挂着 - 所有批量操作必须带分页逻辑,不允许“全表扫完再 commit”;分页必须基于索引字段(最好是主键),避免
OFFSET跳跃 - 监控要常态化:每天跑一次
INNODB_TRX查询,把duration_sec > 300的事务自动告警,而不是等磁盘报警才介入
SHOW ENGINE INNODB STATUS\G 里的 PURGE DONE for trx's n:o 是否在推进,再决定要不要临时调大 innodb_purge_batch_size 或启用 innodb_undo_log_truncate。


















