回滚段与 Purge 线程内核微架构)
MySQL 8.0 事务撤销日志Undo Log回滚段与 Purge 线程内核微架构在 MySQL InnoDB 存储引擎的内核设计中Undo Log撤销日志承担着两大极其神圣的使命事务回滚Transaction Rollback当事务执行ROLLBACK或发生死锁异常中断时利用 Undo Log 中记录的逆向操作将数据恢复至事务开始前的状态多版本并发控制MVCC 一致性非锁定读当查询发起快照读时通过行记录头部的DB_ROLL_PTR回滚指针沿着 Undo Log 版本链自顶向下遍历构建出符合隔离级别的历史快照版本。然而在生产长跑高并发写入场景下Undo Log 也是引发数据库“亚健康慢性死亡”的最常见元凶之一如果线上存在一个长达数小时未提交的长事务Long-Running TransactionInnoDB 的后台垃圾回收线程Purge Thread就会被彻底卡死导致 Undo 表空间体积呈指数级膨胀数十 GB并在 MVCC 遍历时引发毁灭性的 CPU 100% 满载深入 MySQL 8.0 源码storage/innobase/trx/trx0purge.cc看透回滚段Rollback Segment、History List 链表与 Purge 线程的底层物理微架构。[InnoDB Undo Log 回滚段与 Purge 垃圾回收内核全景] ┌─────────────────────────────────────────────────────────────┐ │ InnoDB 系统全局 128 个 Rollback Segments (回滚段内存槽位) │ │ - 每个回滚段包含 1024 个 Undo Slots (单实例支持 128K 并发事务)│ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 事务提交 (Commit): │ │ - TRX_UNDO_INSERT: 提交后直接释放, 0 挂入链表 │ │ - TRX_UNDO_MODIFY: 挂入全局 History List (供 MVCC 读快照) │ └──────────────────────────────┬──────────────────────────────┘ │ ▼ (后台 Purge 线程持续异步推进) ┌─────────────────────────────────────────────────────────────┐ │ Purge 线程物理回收微架构 (trx_purge()): │ │ - 抓取全系统当前最老的活跃 ReadView 事务视图 │ │ - 清理所有提交时间戳早于该 ReadView 的历史 Undo 页面 │ │ - 【若存在长事务未提交 ──▶ Purge 被卡死! Undo 空间打爆!】 │ └─────────────────────────────────────────────────────────────┘源码拆解一两种不同的 Undo Log 生命周期在storage/innobase/trx/trx0undo.cc中InnoDB 对事务操作类型做了极其严密的物理区分TRX_UNDO_INSERT插入撤销日志记录新插入记录的物理主键信息极速生命周期由于新插入的记录在未提交前对其他事务完全不可见MVCC 读无需访问它的前序版本事务一旦执行COMMIT该 Undo 页立即被标记为可重用发生 0 挂入 History List 的开销TRX_UNDO_MODIFY更新与删除撤销日志记录被修改字段的旧值镜像MVCC 依赖生命周期事务提交后不能立即物理删除内核将其挂入回滚段的History List链表中供后续并发的一致性快照读Consistent Read沿着DB_ROLL_PTR链表回溯历史版本。源码拆解二Purge 线程的物理回收机制trx_purge()在storage/innobase/trx/trx0purge.cc中Purge 线程在后台不知疲倦地执行异步内存与磁盘空间回收// Purge 线程垃圾回收推进核心源码逻辑 (storage/innobase/trx/trx0purge.cc) ulint trx_purge(ulint n_purge_threads, bool truncate) { // 1. 获取当前系统中最古老的活跃视图 (Oldest Active ReadView) 的低水位快照 trx_id_t purge_up_to_trx_no trx_sys_get_purge_up_to_trx_no(); // 2. 遍历 History List 历史链表 while (undo_page trx_purge_get_next_page(purge_up_to_trx_no)) { // 2.1 物理删除已被标记为 Delete-Mark 的旧记录 (从 B 树叶子节点中物理抹除) row_purge_step(undo_page); // 2.2 释放不再被任何活跃事务依赖的 Undo 物理数据页 trx_undo_mem_free(undo_page); } // 3. 若开启了 undo_log_truncate且文件超过阈值后台执行物理表空间收缩 if (truncate srv_undo_log_truncate) { trx_purge_truncate_history_list(); } }阻塞死穴Purge 线程能够物理清理的最高事务编号严格受限于“全系统中最早开始且尚未提交的那个活跃 ReadView”只要线上存在一个长事务长跑了 3 个小时整整 3 个小时内产生的所有 Undo Log 全部无法被 Purge 线程回收导致History List LengthHLL瞬间飙升至数百万引发系统严重退化[长事务未提交引发的 Undo 堆积与 MVCC 性能雪崩] 事务 T1 (Begin, 未提交长跑 3 小时) ──▶ 【卡住全局 Purge 推进水位!】 高并发更新写入 ──▶ 生成海量 Undo Log ──▶ History List 暴涨至 5,000,000! │ ▼ (严重后果) ┌─────────────────────────────────────────────────────────────┐ │ 1. 任意简单 SELECT 必须沿着 500 万层回滚链深层回溯 (耗时 5s!)│ │ 2. Undo 表空间物理文件膨胀至 100GB 打满磁盘! │ │ 3. CPU 水位瞬间被链表遍历打满至 100%! │ └─────────────────────────────────────────────────────────────┘生产级参数加固与长事务自动熔断为了彻底杜绝 Undo 膨胀隐患我们在 MySQL 8.0 核心主库上实施了严格的内核参数与自动熔断策略-- 1. 开启独立 Undo 表空间与后台全自动物理收缩 (Undo Truncate) SET GLOBAL innodb_undo_tablespaces 4; -- 声明 4 个独立 Undo 表空间 SET GLOBAL innodb_undo_log_truncate ON; -- 开启自动表空间物理收缩 SET GLOBAL innodb_max_undo_log_size 1073741824; -- 单文件超过 1GB 自动触发后台异步重建收缩 SET GLOBAL innodb_purge_threads 4; -- 分配 4 个专用 Purge 并发回收线程 -- 2. 会话级长事务硬防线: 超过 300 秒未提交的长事务自动强制 Kill SET GLOBAL max_execution_time 30000; -- 只读查询 30 秒硬超时总结深入理解 Undo Log 的回滚段槽位管理与 Purge 线程回收边界配置全自动 Truncate 机制才能在持续数天的大促长跑中守住核心数据库 MVCC 一致性读的极致平稳。