Undo Log 是 InnoDB 十分重要的组成部分,它的作用横贯 InnoDB 中两个最主要的部分:并发控制 (Concurrency Control)和故障恢复(Crash Recovery)。InnoDB 中 Undo Log 的实现亦日志亦数据。
本文将从其作用、设计思路、记录内容、组织结构,以及各种功能实现等方面,整体介绍 InnoDB 中的 Undo Log。文章会深入一定的代码实现,但在细节上还是希望用抽象的实现思路代替具体的代码。
🎯 目录
- [Undo Log 的作用](#Undo Log 的作用)
- [Undo Log 的设计思路](#Undo Log 的设计思路)
- [Undo Record 中的内容](#Undo Record 中的内容)
- [Undo Record 的组织方式](#Undo Record 的组织方式)
- [Undo 的写入流程](#Undo 的写入流程)
- [Undo for Rollback](#Undo for Rollback)
- [Undo for MVCC](#Undo for MVCC)
- [Undo for Crash Recovery](#Undo for Crash Recovery)
- [Undo 的清理](#Undo 的清理)
- 总结
- 参考文献
1️⃣ Undo Log 的作用
1.1 事务回滚
在设计数据库时,我们假设数据库可能在任何时刻,由于如硬件故障、软件 Bug、运维操作等原因突然崩溃。这时尚未完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对 Atomic(原子性)的保证。
直观想法:等到事务真正提交时,才能允许这个事务的任何修改落盘(No-Steal 策略)。
问题:
- 造成很大的内存空间压力
- 提交时的大量随机 IO 会极大影响性能
InnoDB 的解决方案:在正常事务进行中,就不断地连续写入 Undo Log,来记录本次修改之前的历史值。当 Crash 真正发生时,可以在 Recovery 过程中通过回放 Undo Log 将未提交事务的修改抹掉。
额外收益:既然已经有了在 Crash Recovery 时支持事务回滚的 Undo Log,自然地,在正常运行过程中,死锁处理或用户请求的事务回滚也可以利用这部分数据来完成。
1.2 MVCC(Multi-Version Concurrency Control)
为了避免只读事务与写事务之间的冲突,避免写操作等待读操作,几乎所有的主流数据库都采用了多版本并发控制(MVCC)的方式:
- 为每条记录保存多份历史数据供读事务访问
- 新的写入只需要添加新的版本即可,无需等待
InnoDB 的做法:复用 Undo Log 中已经记录的历史版本数据来满足 MVCC 的需求。
2️⃣ Undo Log 的设计思路
🔄 与 Redo Log 的对比
| 特性 | Redo Log | Undo Log |
|---|---|---|
| 目的 | Crash Recovery | Rollback + MVCC |
| 组织方式 | 基于 Page 的物理日志 | 基于事务的逻辑日志 |
| 并发需求 | 写入并发 | 事务间并发 + 多版本维护 |
| 重放逻辑 | 依赖物理存储 | 不依赖物理存储变化 |
| 恢复方式 | 前台同步恢复 | 后台异步回滚 |
💡 为什么选择 Logical Logging?
- 支持 MVCC:DB 运行过程中允许有历史版本的数据存在
- 异步回滚:Crash Recovery 时可以在后台异步回滚,让数据库先恢复服务
- 事务并发:需要的是事务之间的并发,以及方便的多版本数据维护
- 物理无关:重放逻辑不希望因 DB 的物理存储变化而变化
📦 Undo Data 的概念
更多的责任意味着更复杂的管理逻辑,InnoDB 其实是把 Undo 当做一种数据来维护和使用的:
- Undo Log 日志本身也像其他的数据库数据一样
- 会写自己对应的 Redo Log
- 通过 Redo Log 来保证自己的原子性
因此,更合适的称呼应该是 Undo Data。
3️⃣ Undo Record 中的内容
每当 InnoDB 中需要修改某个 Record 时,都会将其历史版本写入一个 Undo Log 中。根据操作类型不同,Undo Record 分为两类:
3.1 Insert 类型的 Undo Record
对应类型 :TRX_UNDO_INSERT_REC
特点:
- 仅为可能的事务回滚准备
- 不在 MVCC 功能中承担作用
- 只需要记录对应 Record 的 Key,供回滚时查找 Record 位置
结构:
text
┌─────────────────────────────────────────────────────────┐
│ Prev Record Offset (2B) │ Next Record Offset (2B) │
├─────────────────────────────────────────────────────────┤
│ Undo Number │ Table ID │
├─────────────────────────────────────────────────────────┤
│ Key Fields (变长) - 记录完整的主键信息 │
└─────────────────────────────────────────────────────────┘
字段说明:
- Undo Number:Undo 的递增编号
- Table ID:表示是哪张表的修改
- Key Fields:长度不定,因为对应表的主键可能由多个 field 组成
- 头尾各 2 字节:记录其前序和后继 Undo Record 的位置
3.2 Update 类型的 Undo Record
背景:由于 MVCC 需要保留 Record 的多个历史版本,当某个 Record 的历史版本还在被使用时,这个 Record 是不能被真正删除的。
三种子类型:
TRX_UNDO_UPD_EXIST_REC:更新已存在的记录TRX_UNDO_DEL_MARK_REC:标记删除记录TRX_UNDO_UPD_DEL_REC:取消删除标记(重新插入)
结构:
text
┌─────────────────────────────────────────────────────────┐
│ Prev Record Offset (2B) │ Next Record Offset (2B) │
├─────────────────────────────────────────────────────────┤
│ Undo Number │ Table ID │
├─────────────────────────────────────────────────────────┤
│ Transaction ID (Trx_id) │ Rollptr │
├─────────────────────────────────────────────────────────┤
│ Key Fields (变长) - 主键信息 │
├─────────────────────────────────────────────────────────┤
│ Update Fields (变长) - 被修改的字段信息 │
└─────────────────────────────────────────────────────────┘
字段说明:
- Transaction ID:产生这个历史版本的事务 ID,用于 MVCC 版本可见性判断
- Rollptr:指向该记录的上一个版本的位置(space number, page number, page 内 offset)
- Update Fields :记录当前 Record 版本相对于其之后一次修改的 Delta 信息
- 所有被修改的 Field 的编号
- 长度
- 历史值
4️⃣ Undo Record 的组织方式
4.1 逻辑组织方式 - Undo Log
每个事务会修改一组 Record,产生一组 Undo Record,这些 Undo Record 首尾相连组成这个事务的 Undo Log。
结构:
text
┌─────────────────────────────────────────────────────────┐
│ Undo Log Header │
├─────────────────────────────────────────────────────────┤
│ Undo Record 1 │
├─────────────────────────────────────────────────────────┤
│ Undo Record 2 │
├─────────────────────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────────────────────┤
│ Undo Record N │
└─────────────────────────────────────────────────────────┘
Undo Log Header 内容:
| 字段 | 说明 |
|---|---|
| Trx ID | 产生这个 Undo Log 的事务 ID |
| Trx No | 事务的提交顺序,用于判断是否能 Purge |
| Delete Mark | 标明该 Undo Log 中是否有 TRX_UNDO_DEL_MARK_REC 类型 |
| Log Start Offset | Undo Log Header 的结束位置 |
| Flags | 一些标志位 |
| Next/Prev Undo Log | 前后两个 Undo Log 的指针 |
| History List Node | 挂载到为 Purge 准备的 History List |
MVCC 版本链:
text
索引 Record
↓ (Rollptr)
Undo Record (最新版本)
↓ (Rollptr)
Undo Record (中间版本)
↓ (Rollptr)
Undo Record (最老版本)
↓ (Rollptr)
NULL
4.2 物理组织格式 - Undo Segment
问题:每个事务产生的 Undo Log 大小不可控,而磁盘写入按固定块大小(默认 16KB)。
解决方案:让多个较小的 Undo Log 紧凑存在一个 Undo Page 中,对较大的 Undo Log 则按需分配多个 Undo Page。
Undo Segment 结构:
text
┌─────────────────────────────────────────────────────────┐
│ Undo Page 1 (First Page) │
├─────────────────────────────────────────────────────────┤
│ Undo Page Header (38-56B) │
│ Undo Segment Header (56-86B) - 仅第一个 Page 有 │
├─────────────────────────────────────────────────────────┤
│ Undo Log 1 (较短) │
│ Undo Log 2 (较短) │
│ ... │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Undo Page 2 │
├─────────────────────────────────────────────────────────┤
│ Undo Page Header │
├─────────────────────────────────────────────────────────┤
│ Undo Log 3 (较长,跨 Page) │
└─────────────────────────────────────────────────────────┘
关键组件:
| 组件 | 作用 |
|---|---|
| Undo Page Header | 记录 Page 类型、最后一条 Undo Record 位置、空闲空间起始位置 |
| Undo Segment Header | 磁盘空间管理的 Handle,记录状态、最后一条 Undo Record 位置、FSP Segment Header、Page 链表 |
| FSP Segment | 管理 16KB Page 的申请和释放 |
空间复用策略:
- 较短的 Undo Log:复用 Undo Page 存放多个 Undo Log
- 较长的 Undo Log:分配多个 Undo Page
- 注意:Undo Page 的复用只会发生在第一个 Page
4.3 文件组织方式 - Undo Tablespace
层级结构:
Undo Tablespace (文件)
↓
Rollback Segment (128 个/表空间)
↓
Undo Segment (1024 个/ Rollback Segment)
↓
Undo Page (16KB)
↓
Undo Log
↓
Undo Record
详细结构:
text
Undo Tablespace File
├── Page 0: File Header
├── Page 1: FSP Header
├── Page 2: Rollback Segment Array Header (目录)
│ └── 128 个指针 → 各个 Rollback Segment Header
│
└── Rollback Segment Header Page
├── 1024 个 Slot (每个 4 字节)
│ └── 指向 Undo Segment 的 First Page
└── History List (已提交事务的链表)
MySQL 8.0 的改进:
- 支持最多 127 个独立的 Undo Tablespace
- 避免了
ibdata1的膨胀 - 方便 Undo 空间回收
- 大大增加了最大 Rollback Segment 个数
- 增加了可支持的最大并发写事务数
4.4 内存组织结构
对应关系:
| 磁盘结构 | 内存结构 | 说明 |
|---|---|---|
| Undo Tablespace | undo::Tablespace |
每个表空间一个 |
| Rollback Segment | trx_rseg_t |
维护 4 个链表 |
| Undo Segment | trx_undo_t |
对应一个 Undo Segment |
trx_rseg_t 的四个链表:
| 链表 | 说明 |
|---|---|
| Update List | 正在被使用的用于写入 Update 类型 Undo 的 Undo Segment |
| Update Cache List | 空闲空间较多,可被后续事务复用的 Update 类型 Undo Segment |
| Insert List | 正在使用中的 Insert 类型 Undo Segment |
| Insert Cache List | 空闲空间较多,可被后续复用的 Insert 类型 Undo Segment |
5️⃣ Undo 的写入流程
5.1 事务初始化
cpp
// 分配 Rollback Segment
trx_assign_rseg_durable(trx);
// 事务的内存结构指向对应的 trx_rseg_t
trx->rsegs = trx_rseg_t;
分配策略:依次尝试下一个 Active 的 Rollback Segment
5.2 获取 Undo Segment
cpp
// 优先复用 Cached List 中的 trx_undo_t
trx_undo_assign_undo(trx);
// 如果没有可用的,创建新的
trx_undo_create(trx);
创建新 Undo Segment 的步骤:
- 轮询选择当前 Rollback Segment 中可用的 Slot(FIL_NULL)
- 申请新的 Undo Page
- 初始化 Undo Page Header、Undo Segment Header
- 创建
trx_undo_t内存结构并挂到trx_rseg_t的对应 List 中
5.3 写入 Undo Record
cpp
// 初始化 Undo Log Header
trx_undo_report_row_operation(trx, undo);
// 根据类型分别处理
if (is_insert) {
trx_undo_page_report_insert(undo, rec);
} else {
trx_undo_page_report_modify(undo, rec);
}
写入内容:
- Insert 类型:记录插入 Record 的主键
- Update 类型:记录主键 + update fields(历史值与当前值的 diff)
返回值:指向当前 Undo Record 位置的 Rollptr,会写入索引的 Record 上
5.4 跨 Page 处理
cpp
// 当前 Page 写满后,添加新的 Page
trx_undo_add_page(undo);
// 新 Page 写入 Undo Page Header 后继续写入
限制:单条 Undo Record 不跨 Page,如果当前 Page 放不下,会将整个 Undo Record 写入下一个 Page
5.5 事务结束
判断条件:
- 只占用了一个 Undo Page
- 当前 Undo Page 使用空间 < Page 的 3/4
处理策略:
| Undo Segment 类型 | 使用空间 < 3/4 | 使用空间 ≥ 3/4 |
|---|---|---|
| Insert | 加入 Cached List | 直接回收 |
| Update | 加入 Cached List | 等待 Purge 后回收 |
State 转换:
| 原状态 | 新状态 | 说明 |
|---|---|---|
TRX_UNDO_ACTIVE |
TRX_UNDO_CACHED |
缓存复用 |
TRX_UNDO_ACTIVE |
TRX_UNDO_TO_FREE |
Insert 直接回收 |
TRX_UNDO_ACTIVE |
TRX_UNDO_TO_PURGE |
Update 等待 Purge |
重要:这个修改对应的 Redo 落盘之后,就可以返回用户结果,Crash Recovery 后也不会再做回滚处理。
6️⃣ Undo for Rollback
6.1 回滚场景
- 用户主动触发 Rollback
- 遇到死锁异常 Rollback
- Crash 后重启,对未提交事务回滚
6.2 回滚流程
入口函数 :row_undo()
步骤:
cpp
// 1. 获取并删除该事务的最后一条 Undo Record
trx_roll_pop_top_rec_of_trx(trx);
// 2. 从后向前依次处理每条 Undo Record
while (undo_rec != NULL) {
// 解析 Undo Record
row_undo_ins_parse_undo_rec(undo_rec, &table, &key, &update_vector);
// 根据类型执行回滚
if (type == TRX_UNDO_INSERT_REC) {
row_undo_ins(undo_rec); // Insert 的逆操作是 Delete
} else {
row_undo_mod(undo_rec); // Update 的逆操作
}
// 获取前一条 Undo Record
undo_rec = get_prev_undo_rec(undo_rec);
}
// 3. 回收完成回滚的 Undo Log 部分
trx_roll_try_truncate(trx);
6.3 Insert 回滚
函数 :row_undo_ins()
步骤:
cpp
// 1. 根据主键定位到对应的 ROW
row_undo_search_clust_to_pcur(table, key, &pcur);
// 2. 在二级索引上删除
row_undo_ins_remove_sec_rec(pcur);
// 3. 在主索引上删除
row_undo_ins_remove_clust_rec(pcur);
6.4 Update 回滚
函数 :row_undo_mod()
步骤:
cpp
// 1. 回退二级索引上的影响
row_undo_mod_del_unmark_sec_and_undo_update(pcur, update_vector);
// 可能包括:
// - 重新插入被删除的二级索引记录
// - 去除 Delete Mark 标记
// - 用 update vector 中的 diff 信息修改回之前的值
// 2. 修改主索引记录回之前的值
row_undo_mod_clust(pcur, update_vector);
6.5 跨 Page 回滚
场景:Undo Log 横跨多个 Undo Page
处理流程:
text
1. 从 Undo Segment Header 中的 Page List 找到最后一个 Undo Page
2. 根据 Undo Page Header 中的 Free Space Offset 定位最后一条 Undo Record
3. 利用 Prev Record Offset 找到前一条 Undo Record
4. 处理完当前 Page 的所有 Undo Records 后
5. 沿着 Undo Page Header 中的 List 找到前一个 Undo Page
6. 重复上述过程
7️⃣ Undo for MVCC
7.1 MVCC 的核心思想
目标:避免写事务和读事务的互相等待
实现方式:
- 读事务在第一次读取时获取一份 ReadView
- ReadView 记录所有当前活跃的写事务 ID
- 通过 ReadView 判断哪些版本可见
ReadView 关键字段:
| 字段 | 说明 |
|---|---|
m_low_limit_id |
当前系统最大事务 ID + 1 |
m_up_limit_id |
活跃事务列表中最小的事务 ID |
m_ids |
活跃事务 ID 列表 |
m_creator_trx_id |
创建该 ReadView 的事务 ID |
7.2 版本可见性判断
判断逻辑:
cpp
bool changes_visible(trx_id_t id, const ReadView* view) {
if (id < view->m_up_limit_id) {
// 事务在 ReadView 创建前已提交,可见
return true;
}
if (id >= view->m_low_limit_id) {
// 事务在 ReadView 创建后才开始,不可见
return false;
}
// 在活跃事务列表中,不可见
return !view->m_ids.contains(id);
}
7.3 历史版本查找
场景示例:
text
事务 R 开始查询 id=1 的记录
├── ReadView 记录:活跃事务 J,I 已提交,K 未开始
│
└── 索引中找到 Record [1, C] (trx_id = K)
├── 不可见(K 在 ReadView 创建后开始)
├── 沿 Rollptr 找到 [1, B] (trx_id = J)
│ ├── 不可见(J 是活跃事务)
│ └── 沿 Rollptr 找到 [1, A] (trx_id = I)
│ └── 可见(I 已提交)
│ └── 返回结果 [1, A]
7.4 历史版本构建
入口函数 :row_search_mvcc()
步骤:
cpp
while (true) {
// 1. 根据 rollptr 找到对应的 Undo Record
trx_undo_prev_version_build(rec, rollptr, &undo_rec);
// 2. 检查是否到头
if (undo_rec_type == TRX_UNDO_INSERT_REC || undo_rec == NULL) {
break; // 到头了
}
// 3. 解析 Undo Record
parse_undo_rec(undo_rec, &trx_id, &rollptr, &key, &update_vector);
// 4. 用 update vector 修改当前持有的 Record 拷贝
row_upd_rec_in_place(rec_copy, update_vector);
// 5. 判断可见性
if (read_view->changes_visible(trx_id)) {
return rec_copy; // 找到可见版本
}
// 6. 继续找更老的版本
}
关键点:
- Undo Record 记录的是 diff 信息
- 需要通过
row_upd_rec_in_place用 update vector 修改当前 Record 拷贝 - 获得完整的历史版本后再判断可见性
8️⃣ Undo for Crash Recovery
8.1 Undo 的持久化
重要概念:InnoDB 中的 Undo 其实是像数据一样处理的,其 Durability 需要靠 Redo 来保证。
Undo 对应的 Redo 类型:
| Redo Type | 触发时机 | 说明 |
|---|---|---|
MLOG_UNDO_INIT |
Undo Page 初始化 | 记录初始化 |
MLOG_UNDO_HDR_REUSE |
重用 Undo Log Header | - |
MLOG_UNDO_HDR_CREATE |
创建新的 Undo Log Header | - |
MLOG_UNDO_INSERT |
写入新的 Undo Record | 最常见 |
MLOG_UNDO_ERASE_END |
Undo Log 跨 Page 时 | 抹除不完整的 Undo Record |
8.2 Crash Recovery 流程
阶段一:重放 Redo Log
text
1. 重放所有 Redo Log
2. Undo 的磁盘组织结构作为数据类型被恢复
3. 包括:
- Undo Tablespace
- Rollback Segment
- Undo Segment
- Undo Page
- Undo Record
阶段二:初始化事务系统
cpp
// 扫描 Undo 的磁盘结构
trx_sys_init_at_db_start();
// 遍历所有 Rollback Segment 和 Undo Segment
for (rseg : rollback_segments) {
for (undo_seg : rseg.undo_segments) {
// 读取 Undo Segment Header 中的 State
state = undo_seg.header.state;
if (state == TRX_UNDO_ACTIVE) {
// 事务需要回滚
mark_for_rollback(trx_id);
} else {
// 事务已结束,继续清理逻辑
mark_for_purge(trx_id);
}
}
}
// 恢复 Undo Log 的内存组织模式
// - 活跃事务的内存结构 trx_t
// - Rollback Segment 的内存结构 trx_rseg_t
// - trx_undo_t 的四个链表
阶段三:异步回滚
cpp
// 启动异步回滚线程
srv_dict_recover_on_restart();
trx_recovery_rollback_thread();
// 对 Crash 前还活跃的事务进行回滚
for (trx : active_transactions) {
trx_rollback_active(trx); // 与正常 Rollback 一致
}
9️⃣ Undo 的清理
9.1 清理时机判断
核心思想:当某个历史版本确认不会被任何现有和未来的事务看到时,就应该被清理。
判断条件:
cpp
// 事务结束时获取递增的 trx_no
trx->no = next_trx_no++;
// 读事务的 ReadView 记录开始时看到的最大 trx_no
read_view->m_low_limit_no = current_max_trx_no;
// 如果一个事务的 trx_no < 所有活跃读事务 ReadView 中的 m_low_limit_no
// 说明这个事务在所有读开始之前已提交,其 Undo Log 可以清理
if (trx->no < min_active_read_view_low_limit_no) {
mark_undo_for_purge(trx);
}
示例:
text
ReadView List:
├── ReadView 1: m_low_limit_no = 100 (最老的)
├── ReadView 2: m_low_limit_no = 150
└── ReadView 3: m_low_limit_no = 200
事务 J: trx_no = 95
├── 95 < 100 (所有 ReadView 的最小 m_low_limit_no)
└── 所有读事务都能看到 J 提交后的版本
└── J 的 Undo Log 可以清理
9.2 清理架构
后台线程:
| 线程 | 职责 |
|---|---|
srv_purge_coordinator_thread |
扫描和分发清理任务 |
srv_worker_thread (多个) |
真正执行清理工作 |
配置参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
innodb_purge_batch_size |
每批处理的 Undo Records 数量 | 300 |
innodb_rseg_truncate_frequency |
Undo Truncate 的频次 | 128 |
9.3 扫描一批要清理的 Undo Records
数据结构 :purge_queue(最小堆)
作用:从多个 History List 中找到 trx_no 全局有序的序列
处理流程:
cpp
// 1. 从 purge_queue 中 pop 出拥有全局最小 trx_no 的 Undo Log
undo_log = trx_purge_choose_next_log(purge_queue);
// 2. 遍历对应的 Undo Log,处理每一条 Undo Record
while (undo_rec = trx_purge_get_next_rec(undo_log)) {
// 分发给 worker 线程处理
assign_to_worker(undo_rec);
}
// 3. 将当前 Rollback Segment 的下一条 Undo Log push 进 purge_queue
next_log = trx_purge_rseg_get_next_history_log(rseg);
push_into_purge_queue(purge_queue, next_log);
// 4. 重复上述过程
示例执行序列:
text
[trx_purge_choose_next_log] Pop T1 from purge_queue;
[trx_purge_get_next_rec] Iterator T1;
[trx_purge_rseg_get_next_history_log] Get T1 next: T5;
[trx_purge_choose_next_log] Push T5 into purge_queue;
[trx_purge_choose_next_log] Pop T4 from purge_queue;
[trx_purge_get_next_rec] Iterator T4;
[trx_purge_rseg_get_next_history_log] Get T4 next: ...;
[trx_purge_choose_next_log] Push ... into purge_queue;
[trx_purge_choose_next_log] Pop T5 from purge_queue;
[trx_purge_get_next_rec] Iterator T5;
[trx_purge_rseg_get_next_history_log] Get T5 next: T6;
[trx_purge_choose_next_log] Push T6 into purge_queue;
......
9.4 Undo Purge(删除索引记录)
目标:真正删除索引上被标记为 Delete Mark 的 Record
处理流程:
cpp
// worker 线程循环处理 coordinator 分配的 Undo Records
while (undo_rec = get_next_undo_rec()) {
// 1. 解析 Undo Record
row_purge_parse_undo_rec(undo_rec, &type, &table_id, &rollptr, &key, &update_vector);
// 2. 针对 TRX_UNDO_DEL_MARK_REC 类型
if (type == TRX_UNDO_DEL_MARK_REC) {
// 从所有二级索引上删除
row_purge_remove_sec_if_poss(table_id, key);
// 从主索引上删除
row_purge_remove_clust_if_poss(table_id, key);
}
// 3. TRX_UNDO_UPD_EXIST_REC 类型可能需要删除二级索引
if (type == TRX_UNDO_UPD_EXIST_REC) {
row_purge_remove_sec_if_poss(...);
}
}
注意:这一步只删除索引记录,不删除 Undo Record 本身
9.5 Undo Truncate(删除 Undo Record)
触发时机:coordinator 线程等待所有 worker 完成一批 Undo Records 的 Purge 工作后
处理流程:
cpp
// 遍历所有的 Rollback Segment 中的所有 Undo Segment
for (rseg : rollback_segments) {
for (undo_seg : rseg.undo_segments) {
if (undo_seg.state == TRX_UNDO_TO_PURGE) {
// 释放占用的磁盘空间并从 History List 中删除
trx_purge_free_segment(undo_seg);
} else {
// 只从 History List 中删除
trx_purge_remove_log_hd(undo_seg);
}
}
}
频次控制:
cpp
// 不是每次都会进行 Undo Truncate
// 要攒 innodb_rseg_truncate_frequency 个 batch 才进行一次
if (++batch_count % innodb_rseg_truncate_frequency == 0) {
trx_purge_truncate();
}
现象 :从 SHOW ENGINE INNODB STATUS 中看到的 Undo History List 的缩短是跳变的
9.6 Undo Tablespace Truncate(重建表空间)
触发条件:
cpp
if (innodb_trx_purge_truncate &&
undo_tablespace.size > innodb_max_undo_log_size) {
// 标记为 inactive
mark_undo_tablespace_inactive(undo_tablespace);
}
处理流程:
text
1. 标记 Undo Tablespace 为 inactive
└── 该 Tablespace 上的所有 Rollback Segment 不参与新事务分配
2. 等待该文件上所有活跃事务退出
└── 所有 Undo Log 完成 Purge
3. 重建 Tablespace
trx_purge_initiate_truncate(undo_tablespace);
├── 重建文件结构
├── 重建内存结构
└── 重新标记为 active
4. 参与分配给新的事务使用
限制:每一时刻最多有一个 Tablespace 处于 inactive 状态
🔟 总结
📊 Undo Log 的核心价值
| 功能 | 作用 | 实现机制 |
|---|---|---|
| 事务回滚 | 保证原子性 | 记录修改前的历史值 |
| MVCC | 提高并发 | 复用历史版本供读事务访问 |
| Crash Recovery | 保证持久性 | 后台异步回滚未提交事务 |
🏗️ 组织结构层级
Undo Tablespace (文件层)
↓ 支持最多 127 个独立文件
Rollback Segment (分配单元)
↓ 每个表空间最多 128 个
Undo Segment (事务持有)
↓ 每个 Rollback Segment 1024 个 Slot
Undo Page (物理存储)
↓ 16KB 固定大小
Undo Log (逻辑日志)
↓ 一个事务一条
Undo Record (最小单位)
↓ 记录单次修改
🔄 生命周期管理
| 阶段 | 操作 | 线程 |
|---|---|---|
| 创建 | 分配 Undo Segment,写入 Undo Record | 用户线程 |
| 使用 | MVCC 版本查找,事务回滚 | 用户线程 |
| 标记 | 事务提交,加入 History List | 用户线程 |
| 清理 | Purge 索引记录 | Purge Worker |
| 回收 | Truncate Undo Record | Purge Coordinator |
| 重建 | Truncate Undo Tablespace | Purge Coordinator |
💡 设计亮点
- Logical Logging:支持事务并发和 MVCC,不依赖物理存储变化
- Undo Data:像数据一样管理,通过 Redo 保证自身持久性
- 分层组织:从 Tablespace 到 Record 的清晰层级
- 异步清理:Purge 机制避免阻塞用户事务
- 空间复用:Cached List 机制提高空间利用率
- 独立表空间:MySQL 8.0 支持独立 Undo Tablespace,便于管理
理解 Undo Log 不仅有助于 DBA 优化配置(如 innodb_purge_batch_size、innodb_max_undo_log_size),也为开发者理解数据库并发控制和故障恢复机制提供了重要视角。
📚 参考文献
官方文档与源码
技术文章
系列文章
- 《数据库故障恢复机制的前世今生》
- 《浅析数据库并发控制机制》
📅 本文基于 MySQL 8.0 源码分析