MySQL-Undo Log(回滚日志)
Undo Log 是 InnoDB 存储引擎中实现事务的关键组件之一。它与 Redo Log 共同协作,确保了事务的原子性(Atomicity)和一致性(Consistency),同时也是 MySQL 实现多版本并发控制(MVCC) 的基础。
一、什么是 Undo Log?
Undo Log,顾名思义,是一种用于撤销操作的日志。它记录了事务发生之前的数据状态(主要是修改前的旧版本数据)。
当执行一个 DELETE
、UPDATE
或 INSERT
操作时,InnoDB 不仅会生成 Redo Log 用于重做,还会生成相应的 Undo Log。如果事务需要回滚(ROLLBACK
)或者系统崩溃后进行恢复,InnoDB 就可以利用 Undo Log 中的信息,将数据还原到修改前的状态。
核心思想: 在修改任何数据之前,先“留底”,把旧数据拷贝一份到 Undo Log 中。
二、Undo Log 的主要作用
实现事务回滚(原子性)
这是 Undo Log 最直接的作用。当一个事务执行失败或用户显式执行ROLLBACK
时,InnoDB 会读取对应事务的 Undo Log,执行相反的逆操作来撤销更改:对于
INSERT
,逆操作是DELETE
。对于
DELETE
,逆操作是INSERT
。对于
UPDATE
,逆操作是用旧值再UPDATE
回去。
实现多版本并发控制(MVCC)- 一致性读
这是 Undo Log 在现代数据库中最重要、最高频的作用。MVCC 使得读操作(SELECT)不会阻塞写操作(UPDATE/DELETE),写操作也不会阻塞读操作。当某个事务需要读取一行数据时,InnoDB 会找到该行数据的一个“可见”版本。
如果该行数据的最新版本(由某个活跃事务修改)对当前读事务不可见,InnoDB 就会沿着该行记录的 DB_ROLL_PTR(回滚指针),在 Undo Log 中寻找更早的、符合当前事务隔离级别要求的旧版本数据。
这些旧版本数据链(版本链)就存储在 Undo Log 中。因此,一个读请求可能会访问到很久之前的数据快照,这些快照就是通过 Undo Log 构建出来的。
三、Undo Log 的存储与结构
1. 物理存储
存储位置: Undo Log 存储在表空间中。从 MySQL 5.6 开始,可以配置为使用独立的 Undo 表空间 (
.ibu
文件),与系统表空间 (ibdata1
) 分离,方便管理和扩展。回滚段 (Rollback Segments): InnoDB 有 128 个回滚段(Rollback Segments),其中:
第 0 号、第 1 号、第 33-127 号回滚段存在于临时表空间。
第 1-32 号回滚段存在于普通表空间(系统表空间或独立 Undo 表空间)。
每个回滚段管理着多个 Undo Slot,每个 Slot 对应一个 Undo Log Segment。
Purge 机制: Undo Log 不会永远保留。当没有任何事务或快照读需要用到某个旧版本数据时(即该 Undo Log 不再被 MVCC 所需),这个 Undo Log 所占用的空间就可以被回收重用。这个删除过期 Undo Log 的过程由后台的 Purge 线程负责。
2. 逻辑结构 - 版本链
每一行记录(聚簇索引)在 InnoDB 中都包含两个隐藏字段:
DB_TRX_ID
(6字节): 最近一次修改该行数据的事务 ID。DB_ROLL_PTR
(7字节): 回滚指针,指向该行数据的上一个旧版本在 Undo Log 中的位置。
UPDATE
操作会形成一个版本链:
事务 A (Trx-id=100) 修改了某行数据。
修改前,该行的旧数据(包括
DB_TRX_ID
和所有字段值)被拷贝到 Undo Log 中。修改后,新行的
DB_TRX_ID
被设置为 100,DB_ROLL_PTR
指向刚刚创建的 Undo Log 记录。当事务 B (Trx-id=200) 再次修改这行数据时,过程重复:拷贝当前状态到新的 Undo Log 记录,然后更新数据行,并将新的
DB_ROLL_PTR
指向事务 B 创建的 Undo Log 记录。
这样,通过 DB_ROLL_PTR
,所有历史版本的数据就像一条链表一样被串联起来,这就是版本链。
示例:
假设一行数据初始值为 Name=‘Alice’
。
事务 100 将其改为
Name=‘Bob’
。事务 200 又将其改为
Name=‘Charlie’
。
这行记录及其版本链的结构如下:
当前行 (In Table) : [Name='Charlie’, DB_TRX_ID=200, DB_ROLL_PTR --> Undo Record 200]^|
Undo Record 200 : [Name='Bob’, DB_TRX_ID=100, DB_ROLL_PTR --> Undo Record 100]^|
Undo Record 100 : [Name='Alice’, DB_TRX_ID=?, DB_ROLL_PTR -> NULL]
当有一个 Read View 需要查询这行数据时,它会从最新的记录开始,顺着 DB_ROLL_PTR
依次判断哪个版本对它可见。
四、Undo Log 与 Redo Log 的区别
这是一个非常重要的概念,两者的区别和联系如下表所示:
特性 | Redo Log | Undo Log |
---|---|---|
目的 | 重做日志,确保事务的持久性 | 回滚日志,确保事务的原子性和一致性读(MVCC) |
内容 | 记录的是数据页的物理变化(在某个页上做了什么修改) | 记录的是数据修改前的逻辑状态(行的旧值) |
生成时机 | 在事务执行过程中不断写入 | 在数据修改前生成 |
作用时机 | 数据库崩溃恢复时,重放已提交的事务 | 事务回滚时和一致性读(MVCC) 时 |
生命周期 | 事务提交后,对应的 Redo Log 可能很快被覆盖(循环写) | 事务提交后,Undo Log 可能仍被 MVCC 使用,不能立即删除 |
磁盘存储 | 顺序写入(ib_logfile0/1 ) | 随机写入(存在于表空间) |
日志类型 | 物理逻辑日志(物理到页,逻辑到行) | 逻辑日志 |
关键联系: Undo Log 本身的操作(写入、修改)也会产生 Redo Log。因为 Undo Log 也需要持久化,防止在写入 Undo Log 过程中发生崩溃导致数据不一致。这被称为 “Redo Log for Undo Log”。
五、相关参数与最佳实践
innodb_undo_tablespaces
: 设置独立 Undo 表空间的个数。通常建议设置为 2 或更多,便于管理和空间回收。innodb_max_undo_log_size
: 指定每个 Undo 表空间文件的最大大小(默认为 1GB)。超过此值,表空间会被标记为可截断。innodb_undo_log_truncate
: 是否启用自动截断(收缩)Undo 表空间的功能。强烈建议开启(=ON),否则 Undo 表空间会无限增长。innodb_purge_threads
: Purge 线程的数量。在高并发写场景下,可以适当增加此值(如设置为 4)以加快过期 Undo Log 的清理速度。
最佳实践:
启用独立 Undo 表空间和自动截断,避免
ibdata1
文件无限膨胀。对于有大事务或长事务的系统,需要特别关注 Undo Log 的增长。因为一个长时间未提交的事务会阻止 Purge 线程清理它之后产生的所有 Undo Log,可能导致 Undo 表空间急剧增长。
监控
SHOW ENGINE INNODB STATUS\G
输出中的TRANSACTIONS
部分,关注历史链表长度(History list length
),它代表了未 Purge 的 Undo Log 页的数量。
总结
Undo Log 是 InnoDB 引擎的基石之一,它远不止是“回滚”那么简单。它的核心价值在于:
保障原子性:为事务回滚提供基础。
实现 MVCC:构建数据行的多版本,是实现非锁定读(快照读)、提升数据库并发性能的关键。
理解 Undo Log 的工作原理,对于深入掌握 MySQL 的事务机制、MVCC 以及进行性能调优和故障排查都至关重要。