根本原因是trx_state = 'RUNNING'且trx_query IS NULL的长事务未提交,其持有的ReadView强制保留所有旧版本undo日志,导致purge线程无法清理,history list length持续增长;必须优先定位并KILL该类事务,再助推purge进度。

根本原因只有一个:长事务没提交,InnoDB 的 purge 线程被卡死,历史版本 undo 日志无法清理。
trx_state = 'RUNNING' 且 trx_query IS NULL 是真凶
这类事务在 information_schema.INNODB_TRX 里显示为“正在运行”,但 trx_query 是 NULL,说明它早就空闲了,只是没执行 COMMIT 或 ROLLBACK。它持有的 ReadView 会把所有旧版本数据钉住,purge 线程只能干等——History list length 就这么越堆越高。
- 常见于 Python/Java 应用崩溃后连接未关闭、ORM(如 Django/MyBatis)事务边界配置错误、或 try-catch 里漏了 rollback
-
SHOW PROCESSLIST里的Time字段不可信,它只记当前语句耗时,不反映事务开启时间 - 必须查
INNODB_TRX表,按trx_started排序,重点关注超 600 秒的记录
innodb_max_purge_lag 不是清洁工,只是节流阀
innodb_max_purge_lag 参数不会清理已堆积的 undo,它只在未 purge 的 undo 条数超过阈值时,给新写入加微秒级延迟(由 innodb_max_purge_lag_delay 控制),试图“让 purge 赶上”。但它对已卡住的长事务完全无效。
- 设成
0≠ 关闭限流,而是禁用保护,undo 会持续膨胀直到磁盘满 - 设得太小(如 10000)会让正常批量导入也被频繁 throttle,TPS 断崖下跌
- 调完参数却没杀掉那个运行了 3 小时的空闲事务?那所有配置都白搭
KILL 前不看状态,可能让问题更糟
盲目 KILL 是最危险的操作之一,不同 trx_state 对应完全不同的处理逻辑:
-
trx_state = 'RUNNING'且trx_query IS NULL:可安全KILL,99% 是应用侧泄漏 -
trx_state = 'LOCK WAIT':它被别的事务堵住了,得先查INNODB_LOCK_WAITS找出blocking_trx_id,优先干掉上游 -
trx_state = 'ROLLING BACK':千万别动!此时 KILL 会让回滚从同步变异步,IO 更爆、purge 更慢 - 若
History list length > 10000,批量KILL必须加SLEEP(0.2)间隔,否则 purge 线程直接雪崩
杀完事务后,undo 空间也不会立刻释放
杀掉源头事务只是第一步。History list length 不会马上下降,因为 purge 是异步推进的,需要人工助推:
- 先确认
SHOW ENGINE INNODB STATUS\G中 “PURGE DONE for trx's n:o < XXX” 是否在持续递增 - 临时加大清理力度:
SET GLOBAL innodb_purge_batch_size = 10000(默认 300) - 确保已启用独立 undo 表空间:
SHOW VARIABLES LIKE 'innodb_undo_tablespaces'≥ 4(仅 ≥ 2 不够轮换) - 开启自动截断:
SET GLOBAL innodb_undo_log_truncate = ON,再执行ALTER UNDO TABLESPACE undo_001 SET INACTIVE→ 等 History list 明显回落 →ALTER UNDO TABLESPACE undo_001 TRUNCATE
真正难的从来不是怎么缩容 undo,而是怎么在一堆 Sleep 连接里,准确揪出那个早已静默、却仍在阻塞 purge 的事务——它不报错、不写日志、不占 CPU,却能让整个实例缓慢窒息。


















