编程 History list 只涨不降:InnoDB 行版本被谁钉住,先看 innodb_trx 的 trx_started

2026-11-06 00:03:38

History list 只涨不降:先看 innodb_trx 的 trx_started,再谈 purge

现象

SHOW ENGINE INNODB STATUS 的 TRANSACTIONS 段落里,History list length 一路上涨,业务低峰也不回落,重启实例才清零。伴随几个表象:

  • undo 表空间文件持续变大,innodb_undo_log_truncate=ON 也没能截断;
  • 表数据文件膨胀,purge 追不上,磁盘 IO 吃紧、写入变慢;
  • 个别原本走覆盖索引的查询突然要回表,执行计划没变但 rows_examined 涨了。

这几种现象共同指向一件事:某些行的旧版本(update undo)没有被丢弃,而丢弃的前提是没有任何快照还可能读它。

先记住判据:先排 information_schema.INNODB_TRX 里最老的 trx_started,再谈 purge 参数。purge 线程数不够是少见情况,长事务钉住版本链才是常态。

行版本到底是什么

InnoDB 是多版本存储引擎,为已修改的行保留旧版本信息,用于并发读和回滚,这部分信息放在 undo 表空间的回滚段里。

行本身不会被复制成多份。每行内部只有三个隐藏字段:

字段长度含义
DB_TRX_ID6 字节最后插入或更新该行的事务 ID;DELETE 在内部按 update 处理并置删除标记位
DB_ROLL_PTR7 字节回滚指针,指向回滚段中的 undo 日志记录;行被更新时,该 undo 记录包含重建更新前内容所需的信息
DB_ROW_ID6 字节单调递增行 ID,仅在 InnoDB 自动生成聚簇索引时出现在索引里

也就是说,聚簇索引记录是原地更新的,DB_ROLL_PTR 指向的 undo 里存的是前镜像差分,读旧版本时顺着指针回溯。

undo 分两类:

  • insert undo:只在回滚时需要,事务提交即可丢弃;
  • update undo:还要供一致性读使用。只有当不存在「InnoDB 已为其分配快照、且该快照在一致性读中可能需要该 undo」的事务时,才能丢弃。

官方文档明确建议定期提交事务,包括只发一致性读的事务,否则 InnoDB 无法丢弃 update undo,回滚段可能把 undo 表空间撑满。

删除同样不立即物理删除。只有丢弃该删除对应的 update undo 记录时,行和索引记录才真正物理删除,这个动作叫 purge,通常和一条 DELETE 语句耗时同量级。

二级索引的处理和聚簇索引不同:二级索引记录没有隐藏系统列,也不原地更新。索引列被更新时,旧记录被 delete-mark、新记录插入,被标记的记录最终由 purge 清理。当二级索引记录被标记删除、或索引页被更新事务改过时,InnoDB 要回聚簇索引查该记录的 DB_TRX_ID,若该值在读取事务开始后被改过,就得从 undo 取正确版本——此时覆盖索引失效,必须回表。

谁在钉住 undo:ReadView

一个快照(ReadView)的关键字段:

字段含义
m_ids活跃(未提交)事务 ID 列表
m_up_limit_idm_ids 的最小值;m_ids 为空时等于 m_low_limit_id
m_low_limit_id下一个将分配的事务 ID
m_creator_trx_id创建该 ReadView 的事务 ID

可见性判断:

  • trx_id == m_creator_trx_id:可见;
  • trx_id = m_low_limit_id:不可见;
  • 介于两者之间:在 m_ids 内不可见,不在则可见。

RC 隔离级别下每条快照读都新建 ReadView;RR 只在事务第一次快照读时建一次,之后复用(事务内发生过当前读之后,下一次快照读在部分实现里会重建,具体行为建议自行验证)。当前读——SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE、INSERT——读最新已提交版本并加锁,不走 ReadView 的可见性判断。

事务开启序上有一点值得记住:读写事务启动时拿 trx id,结束时拿 no 作为提交序;活跃读写事务集合在 trx_sys->rw_trx_ids,建立快照要在这个 mutex 保护下复制活跃列表。所有 open 的快照挂在 MVCC::m_views 链表上,最老快照就是链表里第一个 open 的快照(快照创建是串行的)。所以只要有一个长事务还抱着旧快照,比它新的 undo 就一律不能丢。

定位顺序与可执行 SQL

第一步:确认 history list 的量级

SHOW ENGINE INNODB STATUS\G
-- TRANSACTIONS 段:History list length

也可以用 metrics(先启用):

SET GLOBAL innodb_monitor_enable = 'trx_rseg_history_len';

SELECT name, count
FROM information_schema.INNODB_METRICS
WHERE name = 'trx_rseg_history_len';

这个计数就是 trx_rseg_history_len。数值本身没有绝对阈值,看的是趋势:长期不回落、且和 undo 表空间增长同步,就值得往下查。

第二步:按 trx_started 排最老的事务

SELECT trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec,
trx_mysql_thread_id,
trx_rows_modified,
trx_tables_locked,
trx_isolation_level,
trx_autocommit_non_locking
FROM information_schema.INNODB_TRX
ORDER BY trx_started ASC;

判断要点:

  • age_sec 远大于业务正常事务耗时(几秒 vs 几十分钟),即为嫌疑对象;
  • trx_rows_modified = 0 且状态为 RUNNING 的,往往是只做一致性读的事务——它照样持有 ReadView,照样钉住 undo,不要跳过;
  • trx_state = 'ROLLBACK' 的单独处理,见下一节;
  • trx_tables_locked 帮助你判断影响面。

第三步:把 trx 映射回连接和 SQL

SELECT id, user, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
WHERE id = ;

time 很大、command 为 Sleep 的连接,通常就是应用侧忘了 commit 或 autocommit=0 后一直挂着的那条。

第四步:排除 purge 自身跟不上

如果第二步里最老事务的 age_sec 只有几秒,但 history list 仍然单边上涨,那问题在 DML 与 purge 的速度差:小批量、插入与删除速率相近的场景,purge 线程会落后,表因大量「死」行不断变大。此时看 innodb_max_purge_lag 是否还是 0(不施加任何延迟),并限制新行操作。

处置

KILL ;

MySQL 8.0 起,KILL 大事务只是打标记,回滚在后台进行,进程状态显示 Rolling back,INNODB_TRX 里 trx_state 变成 ROLLBACK。这段时间 undo 依然不能被 purge,history list 会继续涨,这是正常现象,不要在回滚未完成时反复 KILL 或重启。

参数层面的调节空间:

参数默认值说明
innodb_purge_threads4(1–32)purge 工作线程数
innodb_purge_batch_size300每轮 purge 处理的 undo 记录数
innodb_max_purge_lag00 表示不限制延迟;大于 0 时 history list 超过该值会对 DML 施加延迟
innodb_max_purge_lag_delay0单位微秒,上限 10000000
innodb_undo_log_truncateONundo 表空间截断开关
innodb_purge_rseg_truncate_frequency128回滚段截断频率

innodb_max_purge_lag 的作用是给 DML 限流、把 CPU 让给 purge,属于缓解手段,不解决长事务本身。

应用侧的处理更根本:把大事务拆批;事务里不要夹 RPC、消息投递、文件 IO;一致性读也要放进短事务并及时提交;检查那些开了 autocommit=0 的连接池配置。

常见误判

  • 只调 innodb_purge_threads。 瓶颈在快照不在线程数,只要最老快照不动,加线程也丢不掉 undo。
  • 认为 innodb_undo_log_truncate=ON 就该立刻收缩。 截断要等对应回滚段不再被 purge 需要,长事务在,undo 表空间不会缩。
  • 只看 trx_state = 'RUNNING'。 ROLLBACK 状态的事务同样阻止 purge,且在 INNODB_TRX 里会持续存在一段时间。
  • 忽略只读事务。 trx_rows_modified = 0 不代表无害,一致性读同样建立并持有 ReadView。
  • history list 高就断定是长事务。 也可能是 purge 落后于 DML 速率,两者处置方式完全不同:前者杀事务,后者限流或调 innodb_max_purge_lag。
  • 看到查询变慢先怀疑索引失效。 二级索引记录被 delete-mark、索引页被更新事务改过时,覆盖索引会退化成回表,根因仍是没被清理的版本链,重建索引解决不了。

参考

复制全文 生成海报 行版本 MySQL InnoDB MVCC undo log 长事务

推荐文章

程序员茄子在线接单