MySQL的两大支柱:undo Log&redo log

redo log 和 undo log 是事务型数据库保证 ACID 的两根支柱

一、redo log(重做日志)

1.1 核心作用

保证持久性(Durability),事务提交后,即使数据库崩溃,已提交的数据也能根据redo log 恢复。Redo 负责恢复数据页

1.2 核心原则

先写日志,再刷脏页(WAL-write ahead logging)

innodb的redo log

维度 实现细节
存储文件 ib_logfile0ib_logfile1(默认 2 个,循环覆盖)
内存缓冲 redo log buffer(全局内存)
写入线程 用户线程 → log buffer后台线程/事务提交时刷盘
刷盘策略 innodb_flush_log_at_trx_commit 控制: • 0:每秒刷盘(可能丢 1 秒数据)< • 1(默认):每次事务提交都 fsync(最安全)< • 2:每次提交写入 OS page cache,每秒 fsync
组织方式 物理逻辑日志(Physical-Logical Log),记录的是页内偏移的修改(如:页号 X,偏移 Y 处写入值 Z)
崩溃恢复 实例启动时,从 checkpoint 位置扫描 redo log,重做已提交但未刷盘的事务
两阶段提交 为保证 Binlog 和 Redo Log 一致,XA 事务采用 prepare → commit 两阶段

二、undo log(回滚日志)

2.1 核心作用:

  1. 原子性:事务回滚时,使用undo log恢复旧值
  2. 一致性读/MVCC:快照读时,使用undo log构造版本链,查看历史版本
  3. 崩溃恢复:未提交的事务的修改,通过undo log 回滚。

2.2 innodb 的undo log

维度 实现细节
存储位置 Undo Tablespace(默认 undo_001undo_002,可独立文件)
内存结构 Rollback Segment(回滚段,默认 128 个)→ 每个包含 undo segment ,标记undo状态(活跃/已提交)
日志类型 逻辑日志 ,记录的是反向操作(如:insert 对应 delete,update 对应反向 update)
两种 undo insert undo :事务提交后即可删除(无需 MVCC) • update undo :需保留到无事务需要该旧版本时,由 Purge 线程 异步清理
MVCC 实现 结合 Read View(活跃事务 ID 列表)判断可见性,通过 undo 链回溯历史版本
空间回收 依赖 Purge 操作和 Undo Truncate(定期收缩 undo 表空间)
独立表空间 MySQL 5.6+ 支持 undo 独立表空间,之前放在系统表空间 ibdata1

MySQL的innodb,采用插件式存储引擎,redo/undo是innodb的内部机制,server层还有独立的binlog;innodb redo需要与server层binlog做两阶段提交(XA)保证一致。

事务提交时:

  1. Redo Log 写入,状态设为 PREPARE
  2. Binlog 写入并刷盘
  3. Redo Log 状态改为 COMMIT 这是主从复制不丢数据的关键

2.3 Undo 页本身也受 Redo Log 保护

Undo Log 也是存在磁盘页中的(Undo Tablespace 的数据页)。对 Undo 页的修改(写入新的 Undo 记录、更新 Segment Header 等)同样要先写 Redo Log。

所以崩溃恢复时:

  1. Redo 阶段会先恢复 Undo 页本身的内容(通过 Redo Log)。
  2. 然后基于恢复后的 Undo 页,找到未提交事务进行回滚。

结论 :崩溃恢复不依赖"永久保留所有 Undo",而是依赖"Redo 保护 Undo 页" + "未提交事务的 Undo 不会被清理"。

Undo 负责回滚未提交事务。

一句话串起整个机制

Redo 保证"已提交的不会丢",Undo 保证"未提交的不会脏"。Insert Undo 提交即删是因为它不服务 MVCC;Undo 页受 Redo 保护所以不怕清理;Undo 存的是旧值元数据而非 SQL 文本。

三、undo log和版本链之间的关系

undo log存放的是每行数据的每个版本节点,版本之间使用DB_ROLL_PTR串联起来,DB_ROLL_PTR中存储的是每次生成的undo log在undo tablespace中的偏移量(地址)。

四、示例:一条记录生成undo log、版本链

4.1 场景准备

sql 复制代码
-- 表结构
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    age INT,
    INDEX idx_age (age)   -- 普通索引,让故事更完整
) ENGINE=InnoDB;

-- 初始数据
INSERT INTO users VALUES (1, 'Alice', 20);
COMMIT;

此时磁盘上的数据页(假设在 页号 5):

sql 复制代码
Page 5 (聚簇索引,叶子节点存储完整数据+三个隐藏列):
  id=1 | name='Alice' | age=20 | DB_TRX_ID=50 | DB_ROLL_PTR=NULL

Page 8 (二级索引 idx_age):
  age=20 → 指向 id=1

4.2 事务 A 执行 UPDATE

sql 复制代码
-- 事务 A,事务 ID = 100
BEGIN;
UPDATE users SET age = 30 WHERE id = 1;
-- 暂不提交

Step 1:加载数据页到 Buffer Pool

InnoDB 先把 Page 5(聚簇索引)和 Page 8(二级索引)从磁盘读入 Buffer Pool。

Step 2:生成 Undo Log

事务要修改 age=20 → 30,必须先记住旧值,以便回滚或供其他事务做快照读。

生成的 Undo Log(逻辑日志,存入 Undo Tablespace):

sql 复制代码
Undo Record (Update):
  事务 ID: 100
  表 ID: users
  类型: UPDATE
  主键: id=1
  旧值 (Before Image): age=20, name='Alice'
  变更列位图: [age]
  指针: 指向同一事务对该行的上一条 Undo (此处为null)

Undo 写入后,InnoDB 得到该 Undo 记录的地址,例如:Undo Addr = undo_page_50:offset_512

Step 3:修改聚簇索引记录

Buffer Pool 中的 Page 5 被修改

sql 复制代码
修改前:
  id=1 | name='Alice' | age=20 | DB_TRX_ID=50 | DB_ROLL_PTR=NULL

修改后:
  id=1 | name='Alice' | age=30 | DB_TRX_ID=100 | DB_ROLL_PTR=undo_page_50:offset_512(版本链,记录undo log的地址)

Step 3:版本链变化(此时)

sql 复制代码
版本链:
  [当前记录: age=30, TRX_ID=100, ROLL_PTR→undo_page_50:offset_512(初始版本)]
        │
        ▼
  [初始版本: age=20, TRX_ID=50, ROLL_PTR=NULL]

Step 4:修改二级索引(age 变了,索引要维护)

二级索引 Page 8 也要改:

  • 先删除旧索引项 age=20 → id=1
  • 再插入新索引项 age=30 → id=1

注意:这里的"删除"是标记删除(Purge 时才真正清理),不是物理删除。

Step 5:生成 Redo Log

上述对 Page 5、Page 8 的修改,以及 Undo 页的写入,都要先写 Redo Log。

Redo Log 内容(物理逻辑日志):

复制代码
Redo Record 1 (Undo Page 写入):
  页号: undo_page_50
  偏移: 512
  操作: 写入 Undo Record (事务 100, 旧值 age=20...)

Redo Record 2 (聚簇索引页修改):
  页号: 5
  偏移: 记录槽位
  操作: 将 age 改为 30, DB_TRX_ID 改为 100, DB_ROLL_PTR 改为 undo_page_50:offset_512

Redo Record 3 (二级索引页修改):
  页号: 8
  操作: 标记删除 age=20 项, 插入 age=30 项

这些 Redo 先进入 Redo Log Buffer ,然后按策略刷盘(默认每次事务提交 fsync)。

Step 6:事务提交

sql 复制代码
COMMIT;

此时:

  • Redo Log 强制刷盘(fsync),保证持久化。
  • Binlog(如果开启)写入并刷盘。
  • 行锁释放
  • 事务状态标记为 已提交
  • 脏页(Page 5、Page 8)仍留在 Buffer Pool,不会立即刷盘(由后台线程异步刷)。

提交时,Undo 记录被标记为"事务 200 已提交",但不会被立即删除

4.3 事务 B 执行 UPDATE(串行第二步)

sql 复制代码
-- 事务 B (TRX_ID = 200),在 A 提交后才能拿到 X 锁
BEGIN;
UPDATE users SET age = 50 WHERE id = 1;
COMMIT;

Step 1:生成 Undo Log

复制代码
Undo Record 2 (Update):
  ├─ 事务 ID: 200
  ├─ 旧值: age=30, name='Alice'    ← 注意:存的是 A 修改后的值
  ├─ 位置: undo_page_50:offset_600

这一次生成的undo record的地址是undo_page_50:offset_600

Step 2:修改聚簇索引记录

复制代码
修改前:
  id=1 | age=30 | DB_TRX_ID=100 | DB_ROLL_PTR=undo_page_50:offset_512(事务A)

修改后:
  id=1 | age=50 | DB_TRX_ID=200 | DB_ROLL_PTR=undo_page_50:offset_600

Step 3:版本链变化(B 提交后)

复制代码
版本链:
  [当前记录: age=50, TRX_ID=200, ROLL_PTR→undo_page_50:offset_600(事务B生成的undo record的地址)]
        │
        ▼
  [age=30, TRX_ID=100, ROLL_PTR→undo_page_50:offset_512(事务A生成的undo record 的地址)]
        │
        ▼
  [初始版本: age=20, TRX_ID=50, ROLL_PTR=NULL]

其实,每次对数据做变更,都会在undo tablespace中生成一条undo record,这个undo record有自己的地址,每行记录的DB_ROLL_PTR中存的就是这个undo record的地址。而每个undo record中存了该事务更新前的值、更新的列、上一个undo record的地址等信息。这样多个undo record通过DB_ROLL_PTR字段串联起来形成了了版本链,而undo record存储在undo tablespace中,也形成了undo log。

五、Undo Log 的删除时机(Purge)

现在分两种情况,决定 Undo 何时能删。

情况 1:没有其他事务引用它

假设事务 A 提交后,系统中所有活跃事务的 Read View 都晚于事务 200(即它们启动时,事务 200 已提交)。那么:

  • Purge 线程 发现这条 Undo 不再被任何 Read View 需要。
  • 将其从 Undo Page 中清理,Undo Page 空间可复用。

Insert Undo :提交后立即可被 Purge(因为没有 MVCC 回溯需求)。

Update Undo :必须等到没有活跃事务需要用它构造旧版本时,才能 Purge。

情况 2:有老事务在引用它

假设事务 B(事务 ID = 250)在事务 A 提交之前就已启动,并执行了快照读:

sql 复制代码
-- 事务 B (ID=250),在事务 A 提交前已 BEGIN
SELECT * FROM users WHERE id = 1;  -- 一致性读

事务 B 的 Read View 记录:活跃事务列表包含 200(假设当时 A 还没提交)。

当事务 A 提交后,事务 B 再次执行:

sql 复制代码
SELECT * FROM users WHERE id = 1;  -- 仍然读快照

B 看到 DB_TRX_ID=200,判断 200 对自己不可见,于是顺着 DB_ROLL_PTR 找到 Undo,构造出旧版本 age=20

此时这条 Undo 绝对不能被 Purge!

直到事务 B 提交(或回滚),它的 Read View 消失,Purge 线程才能清理这条 Undo。

六、数据库崩溃与恢复

假设在事务 A 已提交,但脏页还没刷盘时,数据库挂了。

崩溃前状态

位置 状态
磁盘数据页 (Page 5) 仍然是 age=20(脏页未刷盘)
磁盘数据页 (Page 8) 仍然是 age=20
Redo Log 文件 已刷盘,包含 Record 1/2/3
Undo Tablespace 已刷盘(Undo 页的修改也写了 Redo)
Binlog 已刷盘(两阶段提交保证一致)

恢复阶段 1:Redo(Roll Forward)

数据库重启,InnoDB 进入崩溃恢复:

  1. 找到 Redo Log 中最近的 Checkpoint
  2. 从 Checkpoint 开始扫描 Redo Log。
  3. 发现事务 200 的 Redo Record:
    • 重做对 Page 5 的修改(age=30, TRX_ID=200, ROLL_PTR=...
    • 重做对 Page 8 的修改
    • 重做 Undo Page 的写入(确保 Undo 本身也恢复)

此时磁盘数据页被恢复到崩溃前的最新状态,包括已提交和未提交的事务修改。

恢复阶段 2:Undo(Roll Back)

InnoDB 扫描 Rollback Segment Header,检查活跃事务列表:

  • 发现事务 200 在 Redo 中已有 Commit 标记 (或 Binlog 中已记录),判定为已提交
  • 发现事务 200 的 Undo 记录,但事务已提交 → 不需要回滚

如果事务 200 在崩溃前没来得及提交(Redo 中无 Commit 标记):

  • InnoDB 判定 200 为未提交事务
  • 读取它的 Undo:旧值 age=20
  • 回滚:把 Page 5 的 age 改回 20,恢复二级索引,清理 TRX_ID/ROLL_PTR。
  • 释放锁,标记事务为已回滚。

七、完整流程图

复制代码
事务 A (UPDATE id=1 age=20→30)
    │
    ├─► 加载 Page 5, Page 8 到 Buffer Pool
    │
    ├─► 生成 Undo Log (旧值 age=20) ──────► 写入 Undo Page ──► 写 Redo (保护 Undo 页)
    │
    ├─► 修改 Buffer Pool 中 Page 5 (age=30, TRX_ID=200, ROLL_PTR→Undo)
    │   修改 Buffer Pool 中 Page 8 (二级索引)
    │
    ├─► 生成 Redo Log (Page 5/8 的修改) ──► Redo Log Buffer ──► fsync 刷盘
    │
    ├─► COMMIT
    │     Redo 已落盘 ✓
    │     Binlog 已落盘 ✓
    │     脏页仍在 Buffer Pool(未刷盘)
    │
    ▼
[崩溃发生]
    │
    ▼
重启恢复
    │
    ├─ 阶段1: Roll Forward (Redo)
    │     重放 Redo:Page 5 age=30, Page 8 更新, Undo 页恢复
    │     此时数据 = 崩溃前最新状态(可能含未提交脏数据)
    │
    └─ 阶段2: Roll Back (Undo)
          检查活跃事务:
          • 事务 200 已提交?→ 跳过,保留修改
          • 事务 200 未提交?→ 读 Undo (age=20) → 回滚 → 数据还原

八、关键结论速记

问题 答案
Redo 存什么? 数据页的物理逻辑修改(页号、偏移、新值)
Undo 存什么? 事务的逻辑回滚信息(旧值、主键、列位图),不是 SQL 文本
Undo 何时删? Update Undo 等无 Read View 引用后由 Purge 清理;Insert Undo 提交即清理
崩溃后数据在哪? 脏页可能丢了,但 Redo 一定在,所以能重做数据页
Undo 页本身会丢吗? 不会,因为对 Undo 页的修改也写了 Redo,恢复时先恢复 Undo 页,再基于它做回滚
已提交事务要回滚吗? 不需要 。Undo 只用来回滚未提交事务

补充

二级索引(非聚簇索引)的索引项中没有 DB_TRX_ID 和 DB_ROLL_PTR。

当通过二级索引做一致性读时:

  1. 读取二级索引项(如 age=30 → id=1)。
  2. 无法直接判断可见性,必须回表到聚簇索引。
  3. 在聚簇索引上走上述版本链回溯流程,找到对该事务可见的版本。
相关推荐
智航GIS1 小时前
ArcGIS大师之路500技---078文件数据库的加密与解密
数据库·arcgis
音乐宝贝家2 小时前
吉他面板材质怎么选?云杉单板面单吉他配置深度解析
数据库·新媒体运营·产品运营·媒体·材质·内容运营
2401_873479402 小时前
企业安全运营中,如何用IP离线库提前发现失陷主机?三步实现风险画像
网络·数据库·python·tcp/ip·ip
C137的本贾尼2 小时前
InnoDB 页结构与行结构揭秘
mysql
周末也要写八哥3 小时前
数据库安装 | SQL Server2022安装教程及网盘下载地址
数据库
李燚3 小时前
erlang_migrate 架构拆解:behaviour 驱动的多数据库迁移引擎
数据库·postgresql·架构·erlang·migrate·behaviour·erlang_migrate
Jinkxs3 小时前
PostgreSQL - 全文检索的开启与基础使用
数据库·postgresql·全文检索
情绪总是阴雨天~4 小时前
检索增强生成 (RAG) 四大检索策略详解
数据库·prompt·检索增强
学Linux的语莫4 小时前
redis的数据类型和使用
数据库·redis·缓存