本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
日志与恢复(redo / undo / binlog / 两阶段提交) · undo log 与回滚

undo log 是逻辑日志还是物理日志?为什么是逻辑日志?

Is the undo log a logical log or a physical log? Why is it considered a logical log?

答案语言

考察要点

这道题面试官想看你是否深刻理解 InnoDB 日志系统的核心设计哲学,特别是 逻辑日志 与 物理日志 的根本区别。同时,这也是在考察你对事务 原子性(Atomicity) 和 隔离性(Isolation) 底层实现原理(即 MVCC)的掌握程度。高分回答需要清晰阐述 undo log 为何必须是逻辑日志,并能关联到 MVCC 的版本链。

核心概念

undo log(回滚日志)是 InnoDB 存储引擎实现事务原子性和 MVCC 的关键组件。它的核心是记录事务发生前的“数据旧貌”,以便在事务回滚时恢复数据,或在并发场景下为其他事务提供一致性读的快照。

undo log 是逻辑日志。

要理解为什么,首先要区分逻辑日志和物理日志:

特性逻辑日志 (Logical Logging)物理日志 (Physical Logging)
记录内容记录对数据执行的 操作 的逆操作。例如,对于 UPDATE,记录修改前的值;对于 INSERT,记录对应的主键以便 DELETE。记录数据页(Page)上发生的 物理变化。例如,“在 Page A 的偏移量 B 处,将字节 C 改为字节 D”。
日志体积通常更小、更紧凑。可能很大,尤其是在修改大字段或多行时,可能涉及多个数据页的变更记录。
幂等性逻辑回滚操作不一定是幂等的。如果在回滚期间,数据页被其他已提交的事务所修改,直接应用逻辑逆操作是安全的。物理恢复操作通常是幂等的。无论执行多少次,将一个页面的特定位置恢复到某个状态,结果都一样。
典型代表MySQL undo log、MySQL binlog (Statement/Row 格式)MySQL redo log

为什么 undo log 必须是逻辑日志?

核心原因在于 保证并发事务下的正确回滚。

假设 undo log 是物理日志,我们来看一个场景:

  1. 事务 A 启动,UPDATE t SET name = 'B' WHERE id = 1;(原 name 为 'A')。
  2. undo log 记录了包含 id=1 这行数据的数据页 P1 在修改前的 物理镜像。
  3. 事务 B 启动并提交,UPDATE t SET age = 30 WHERE id = 1;。这个修改也发生在数据页 P1 上。
  4. 事务 A 决定回滚。如果 undo log 是物理日志,它会尝试将整个数据页 P1 强制恢复 到步骤 1 之前的状态。
  5. 问题出现:这个恢复操作会把事务 B 已提交的 age = 30 的修改给覆盖掉,破坏了事务 B 的持久性,违反了数据库的隔离性原则。

而如果 undo log 是逻辑日志,情况则完全不同:

  1. 事务 A UPDATE 时,undo log 记录的是一个逻辑记录:{id: 1, old_name: 'A'}。
  2. 事务 B UPDATE 并提交。
  3. 事务 A 回滚时,它执行的是一个逻辑逆操作:UPDATE t SET name = 'A' WHERE id = 1;。
  4. 结果正确:这个操作只修改了 name 字段,完全不影响 age 字段,从而保证了事务 B 的修改不受影响。

undo log 的具体逻辑格式

undo log 根据 DML 操作类型记录不同的逻辑内容:

  • INSERT 操作: undo log 记录下新插入行的主键。回滚时,InnoDB 根据主键执行 DELETE 操作。
    sql
    1-- 事务操作
    2INSERT INTO users (id, name) VALUES (100, 'Jerry');
    3-- undo log 逻辑记录
    4-- { type: TRX_UNDO_INSERT_REC, primary_key: {id: 100} }
    5-- 回滚操作:DELETE FROM users WHERE id = 100;
  • UPDATE 操作: undo log 记录下被修改行的主键以及所有被修改列的旧值。回滚时,InnoDB 根据主键和旧值执行一个反向的 UPDATE。
    sql
    1
    2-- 事务操作(假设原 name='Tom')
    3UPDATE users SET name = 'Thomas' WHERE id = 101;
    4-- undo log 逻辑记录
    5-- { type: TRX_UNDO_UPD_EXIST_REC, primary_key: {id: 101}, old_values: {name: 'Tom'} }
    6-- 回滚操作:UPDATE users SET name = 'Tom' WHERE id = 101;
  • DELETE 操作: undo log 记录下被删除行的所有列的完整内容。回滚时,InnoDB 根据这些内容执行一个 INSERT 操作。
    sql
    1-- 事务操作
    2DELETE FROM users WHERE id = 102;
    3-- undo log 逻辑记录
    4-- { type: TRX_UNDO_DEL_MARK_REC, full_row_content: {id: 102, name: 'Spike', ...} }
    5-- 回滚操作:INSERT INTO users (id, name, ...) VALUES (102, 'Spike', ...);

undo log 与 MVCC

undo log 的逻辑记录构成了 版本链,是 MVCC(多版本并发控制)的基石。InnoDB 表的聚簇索引记录中,除了用户定义的列,还有两个隐藏列:

  • DB_TRX_ID: 最近修改该记录的事务 ID。
  • DB_ROLL_PTR: 指向该记录上一个版本在 undo log 中的位置。

当一个事务需要读取某行数据时,如果发现该行的 DB_TRX_ID 是一个尚未提交的活跃事务,它就会通过 DB_ROLL_PTR 顺着版本链往下找,直到找到一个对其可见的版本(即 DB_TRX_ID 小于当前事务的 Read View ID 或属于已提交事务)。

mermaid
1graph LR
2 subgraph Clustered Index Record
3 A[Row (id=1, name='C', DB_TRX_ID=T3, DB_ROLL_PTR=ptr2)]
4 end
5 subgraph Undo Log
6 B[Undo Record (old_name='B', DB_TRX_ID=T2, DB_ROLL_PTR=ptr1)]
7 C[Undo Record (old_name='A', DB_TRX_ID=T1, DB_ROLL_PTR=NULL)]
8 end
9 A -- "DB_ROLL_PTR" --> B
10 B -- "DB_ROLL_PTR" --> C

这个版本链的存在,完全依赖于 undo log 记录的是逻辑上的“旧值”,而非物理上的“旧页”。

常见低分回答

  1. 混淆 undo log 和 redo log:回答“undo log 是物理日志,用于掉电恢复”,这是完全错误的。掉电恢复保证持久性主要靠 redo log,undo log 在恢复阶段用于回滚未完成的事务。
  2. 回答“既是逻辑也是物理”:这种模棱两可的回答体现了对底层原理的理解不深。虽然日志格式内部可能包含物理地址信息(如 page no, offset),但其核心功用和设计思想是纯逻辑的——记录如何从逻辑上撤销一个操作。
  3. 只谈回滚,不谈 MVCC:只说 undo log 用于事务回滚,说明理解停留在了表面。undo log 在 InnoDB 中同等重要的作用是构建数据多版本,实现非锁定读,这是 MVCC 的核心,也是面试官非常期待听到的点。
  4. 无法解释“为什么”:能说出 undo log 是逻辑日志,但当被追问“为什么不能是物理日志”时,无法举出并发场景下数据被破坏的反例。这表明知识是背诵的,而非理解的。

工程实践与优化

  • 长事务的危害:一个长时间运行的事务会阻止 undo log 的清理(Purge)。因为该事务的 Read View 要求看到数据库在它开始时刻的快照,所以它启动后提交的事务所产生的 undo log 都不能被删除。这会导致 undo log 文件(或表空间)持续膨胀,消耗大量磁盘空间,并降低系统整体性能,因为查询需要遍历更长的版本链。
  • 监控 undo log:通过 SHOW ENGINE INNODB STATUS; 可以查看 History list length,这个值代表了未被清理的 undo log 记录数量。如果该值持续增长,通常意味着存在长事务。
  • undo log 空间管理:
    • 在 MySQL 5.7 及之前,undo log 默认存储在系统表空间 ibdata1 中,难以收缩。
    • 从 MySQL 5.7 开始,可以配置独立的 undo 表空间 (innodb_undo_tablespaces)。
    • MySQL 8.0 进一步增强,默认开启 innodb_undo_log_truncate,允许在线自动收缩(truncate)过大的 undo 表空间,极大地改善了空间管理问题。
  • 避免大事务:在应用开发中,应将大的数据操作(如批量删除、更新)拆分成多个小事务,并尽快提交,以避免产生过长的 undo log 链。

常见追问与延伸题目

  1. 追问:那 redo log 是逻辑日志还是物理日志?它和 undo log 有什么核心区别?

    • 回答要点:redo log(重做日志)通常被认为是 物理日志,但更准确地说是 物理逻辑日志(Physiological Logging)。它记录的是对数据页的逻辑操作(如“在某页的某偏移量插入一行”),而不是纯粹的字节差异(纯物理)。这种设计结合了物理日志的幂等性和逻辑日志的紧凑性。
    • 核心区别:
      对比项undo logredo log
      目的事务回滚 (Atomicity) & MVCC (Isolation)保证已提交事务的持久性 (Durability),用于故障恢复
      内容记录数据修改前的 逻辑 旧值记录数据修改后的 物理 新值(或物理操作)
      写入时机事务执行 DML 语句时,在修改数据页 之前 写入事务执行 DML 语句时,在修改数据页的同时(或之后,提交前)写入
      生命周期事务提交后,当不再被任何 MVCC 快照需要时,即可被后台 Purge 线程清理对应的脏页被刷写到磁盘后,redo log 在日志文件中的空间即可被覆盖重用(循环写)
  2. 追问:详细解释一下 MVCC 中,一个事务是如何利用 undo log 看到旧版本数据的?

    • 回答要点:
      1. Read View 创建:当一个事务首次执行 SELECT(在 REPEATABLE READ 级别)或每次 SELECT(在 READ COMMITTED 级别)时,会创建一个名为 Read View 的快照。Read View 包含当前所有活跃(未提交)事务的 ID 列表。
      2. 可见性检查:当事务读取一行数据时,它会检查该行记录的隐藏列 DB_TRX_ID。
      3. 判断逻辑:
        • 如果 DB_TRX_ID 小于 Read View 中最小的活跃事务 ID,说明该版本在事务开始前就已提交,可见。
        • 如果 DB_TRX_ID 大于等于 Read View 中最大的活跃事务 ID,说明该版本是在事务开始后才创建的,不可见。
        • 如果 DB_TRX_ID 在 Read View 的活跃事务 ID 列表中,说明该版本是由一个还未提交的并发事务修改的,不可见。
      4. 遍历版本链:如果当前版本不可见,事务会通过记录中的 DB_ROLL_PTR 指针,去 undo log 中找到上一个版本,然后对上一个版本重复步骤 2 和 3 的可见性检查,直到找到一个可见的版本或链表结束。
  3. 追问:如果 MySQL 发生崩溃,在重启恢复时,undo log 和 redo log 是如何协作的?

    • 回答要点:
      1. Redo Log 前滚 (Roll Forward):MySQL 重启后,首先会检查 redo log。它会重放所有已经写入 redo log 但尚未刷入磁盘数据文件的操作,将数据恢复到崩溃前的最新状态。这保证了已提交事务的持久性。此时,数据库中可能包含已提交事务的数据和未提交事务的数据。
      2. Undo Log 回滚 (Roll Back):完成 redo log 的前滚后,数据库状态包含了所有已做出的修改。接着,MySQL 会扫描 undo log,找出所有在崩溃时仍处于“活跃”状态(未提交)的事务。
      3. 执行回滚:对这些未提交的事务,MySQL 会利用 undo log 中记录的逻辑旧值,执行逆向操作,将这些事务所做的修改全部撤销。这保证了事务的原子性。
      4. 总结:redo log 负责“重做”已提交的,undo log 负责“撤销”未提交的,两者协作,共同将数据库恢复到一个一致性的状态。