本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
事务与并发(ACID / 隔离级别 / 锁 / MVCC) · MVCC 多版本并发控制

什么是隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR?undo log 链表是怎么形成的?

What are the hidden columns DB_TRX_ID and DB_ROLL_PTR? How is

答案语言

考察要点

这道题面试官想看你是否深刻理解 InnoDB 存储引擎实现多版本并发控制(MVCC)的底层机制。核心是考察你对以下三点的掌握程度:

  1. 数据行版本管理:InnoDB 如何在物理上存储一个逻辑行的多个版本。
  2. 版本链的构建:隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR 如何像指针一样,将一个行的当前版本和其在 undo log 中的历史版本串联起来。
  3. 一致性读的实现:理解了版本链后,如何通过 ReadView 机制决定在当前事务中应该读取哪个版本的数据,从而实现不同隔离级别(如读已提交、可重复读)。

核心概念

InnoDB 中,聚簇索引的每一条记录除了我们定义的列外,还包含几个隐藏字段,其中最重要的就是 DB_TRX_ID 和 DB_ROLL_PTR,它们是实现 MVCC 的基石。

1. 隐藏字段定义

隐藏字段大小(Bytes)描述
DB_ROW_ID6行 ID。当表没有显式定义主键,也没有非 NULL 的唯一索引时,InnoDB 会自动生成一个单调递增的 DB_ROW_ID 作为聚簇索引键。如果存在主键,则该字段不存在。
DB_TRX_ID6事务 ID。记录了最近一次修改(INSERT 或 UPDATE)该行记录的事务 ID。
DB_ROLL_PTR7回滚指针(Roll Pointer)。指向该行记录的上一个版本在 undo log 中的地址。通过这个指针,可以将一个行的所有历史版本串成一个链表。

2. undo log

undo log(回滚日志)在 MVCC 中扮演着双重角色:

  1. 事务回滚:当事务需要回滚时,可以根据 undo log 记录恢复到事务开始前的状态。
  2. 版本构建:为其他事务提供历史版本的数据,实现一致性读。

undo log 记录了行的历史版本。当一个事务对某行进行 UPDATE 操作时,InnoDB 会执行以下操作:

  1. 将原始行数据的拷贝(即旧版本)写入一条 undo log 记录。
  2. 修改聚簇索引中的行记录,更新列的值。
  3. 将该行的 DB_TRX_ID 设置为当前事务的 ID。
  4. 将该行的 DB_ROLL_PTR 指向刚刚写入的 undo log 记录。

3. undo log 链表的形成

下面通过一个具体的例子来展示 undo log 链表是如何形成的。

初始数据 假设有一个 user 表,id 为主键,name 为普通列。

sql
1CREATE TABLE user (
2 id INT PRIMARY KEY,
3 name VARCHAR(255)
4) ENGINE=InnoDB;

一个事务(TRX_ID = 10)插入了一条记录:

sql
1INSERT INTO user (id, name) VALUES (1, 'Alice');

此时,聚簇索引中的记录如下(简化表示):

idnameDB_TRX_IDDB_ROLL_PTR
1'Alice'10NULL

第一次更新 另一个事务(TRX_ID = 20)执行更新:

sql
1UPDATE user SET name = 'Bob' WHERE id = 1;

变化过程:

  1. 记录 undo log:InnoDB 在 undo log 中创建一条记录,包含旧值 name='Alice',以及旧的事务 ID 10 和回滚指针 NULL。
  2. 更新数据行:修改聚簇索引中的记录,将 name 改为 'Bob',DB_TRX_ID 改为 20,并将 DB_ROLL_PTR 指向刚刚创建的 undo log 记录。

当前数据行状态:

idnameDB_TRX_IDDB_ROLL_PTR
1'Bob'20ptr_to_undo_1

undo log 状态:

  • undo_1: { old_name: 'Alice', old_trx_id: 10, old_roll_ptr: NULL }

第二次更新 又一个事务(TRX_ID = 30)执行更新:

sql
1UPDATE user SET name = 'Charlie' WHERE id = 1;

变化过程:

  1. 记录 undo log:在 undo log 中创建新记录,包含旧值 name='Bob',旧的事务 ID 20 和回滚指针 ptr_to_undo_1。
  2. 更新数据行:修改聚簇索引记录,name 改为 'Charlie',DB_TRX_ID 改为 30,DB_ROLL_PTR 指向新的 undo log 记录。

当前数据行状态:

idnameDB_TRX_IDDB_ROLL_PTR
1'Charlie'30ptr_to_undo_2

undo log 状态:

  • undo_2: { old_name: 'Bob', old_trx_id: 20, old_roll_ptr: ptr_to_undo_1 }
  • undo_1: { old_name: 'Alice', old_trx_id: 10, old_roll_ptr: NULL }

版本链图示 这样,一条以数据页中最新记录为头节点,undo log 中历史版本为后续节点的单向链表就形成了。

mermaid
1graph LR
2 A[数据页中当前记录<br>id=1, name='Charlie'<br>DB_TRX_ID=30<br>DB_ROLL_PTR=ptr_to_undo_2] --> B(Undo Log 记录 2<br>name='Bob'<br>DB_TRX_ID=20<br>DB_ROLL_PTR=ptr_to_undo_1);
3 B --> C(Undo Log 记录 1<br>name='Alice'<br>DB_TRX_ID=10<br>DB_ROLL_PTR=NULL);
4 C --> D((链表结束));

当一个事务需要读取 id=1 的记录时,它会获取一个 ReadView(读视图),然后从链表头(当前记录)开始,根据 ReadView 和记录的 DB_TRX_ID 判断该版本是否可见。如果不可见,则通过 DB_ROLL_PTR 顺着链表往下找,直到找到第一个可见的版本为止。

常见低分回答

  1. 混淆 undo log 和 redo log:错误地认为 undo log 用于断电恢复。实际上,redo log 保证事务的持久性(Durability),而 undo log 主要用于事务回滚和实现 MVCC。
  2. 认为 undo log 仅用于回滚:只答出 undo log 用于事务回滚,但完全没提及其在 MVCC 中为实现一致性读提供历史版本数据的核心作用。这是最常见的知识缺漏。
  3. 对版本链描述不清:只说 DB_ROLL_PTR 是个指针,但说不清它指向哪里,以及如何将多个历史版本串联起来形成链表。无法画出或清晰描述上述的版本链形成过程。
  4. 对可见性判断逻辑模糊:知道会去 undo log 里找旧版本,但说不清判断一个版本是否“可见”的具体规则(即 ReadView 的工作原理),只是模糊地说“找一个已提交的版本”。
  5. 认为 DELETE 是物理删除:不了解 InnoDB 的 DELETE 操作实际上是一个“标记删除”(delete mark),其行记录本身并未立即被物理删除,而是像 UPDATE 一样记录 undo log,其物理删除由后台的 Purge 线程异步完成。

工程实践与优化

  • 避免长事务:长事务是 MVCC 的天敌。一个长时间运行的事务会导致其 ReadView 保持活跃,使得系统无法清理该事务启动后产生的大量 undo log。这会导致:

    • undo 表空间持续膨胀,占用大量磁盘空间。
    • 其他查询在遍历版本链时,可能需要回溯很长的距离,严重影响读性能。
    • information_schema.innodb_trx 中可以看到活跃时间很长的事务,History List Length(SHOW ENGINE INNODB STATUS)持续增长是危险信号。
  • 监控 History List Length (HLL):HLL 代表了 undo log 中未被清理的版本数量。一个健康系统的 HLL 应该保持在较低水平并稳定。如果 HLL 持续快速增长,说明有长事务或 Purge 线程受阻,需要立即排查。

  • MySQL 8.0 的 undo 表空间管理:从 MySQL 5.7 开始,undo log 可以配置在独立的 undo 表空间中,而不是全部挤在系统表空间 ibdata1 里。MySQL 8.0 默认开启此项,并支持在线收缩(Truncate)undo 表空间,这极大地改善了 undo 空间管理,避免了 ibdata1 无限膨胀的古老问题。

  • UPDATE 优化:对于 UPDATE 密集的场景,如果更新没有修改主键,并且新旧数据行长度变化不大,InnoDB 可能会触发 原地更新(in-place update) 的优化,直接在原数据页上修改,减少 I/O。但无论如何,undo log 记录是必须的。

常见追问与延伸题目

  1. 追问:Read Committed 和 Repeatable Read 两种隔离级别,在使用这个版本链时有什么区别?

    • 回答要点:区别在于 ReadView 的创建时机。
      • Repeatable Read (RR):在事务开始后的第一个 SELECT 语句执行时创建 ReadView,并且整个事务期间都复用这个 ReadView。因此,事务期间看到的数据快照是固定的,不会看到其他事务在此之后提交的修改。
      • Read Committed (RC):每执行一条 SELECT 语句时都会重新创建一个新的 ReadView。因此,事务内的每次查询都能看到其他事务已经提交的最新修改,可能导致“不可重复读”。
  2. 追问:如果一个事务执行了 DELETE 操作,这条记录在物理上发生了什么?

    • 回答要点:DELETE 在 InnoDB 中被视为一种特殊的 UPDATE。
      • 标记删除:它并不会立即将记录从数据页中物理删除。而是将记录头信息中的一个 delete_mask 标志位置为 1,表示该记录已被删除。同时,像 UPDATE 一样,记录其 DB_TRX_ID 和 DB_ROLL_PTR,并写入 undo log。
      • Purge 线程清理:这条被标记为删除的记录,只有在系统中不再有任何事务(ReadView)能够看到它时(即所有比该删除事务更早启动的事务都已结束),后台的 Purge 线程才会最终将其物理删除,并回收空间。
  3. 追问:什么是 History List Length?它过高意味着什么,如何解决?

    • 回答要点:
      • 定义:History List Length (HLL) 是 undo log 版本链的总长度,代表了等待被 Purge 线程回收的 undo 记录数量。
      • 意味:HLL 过高且持续增长,是数据库性能恶化的一个重要指标。它通常意味着存在一个或多个长事务,这些长事务的 ReadView 阻止了 Purge 线程回收旧的 undo 记录。这会导致 undo 表空间膨胀,并降低需要遍历版本链的查询性能。
      • 解决:
        1. 定位长事务:通过查询 information_schema.innodb_trx,找到 trx_started 时间很早但仍处于 RUNNING 状态的事务。
        2. 分析与处理:分析该事务执行的 SQL,判断其是否是必要的。如果是业务代码 bug 或不合理的设计(如大批量数据处理未分页),应优化代码。如果是手动执行的查询忘记提交,应立即 COMMIT 或 ROLLBACK。在紧急情况下,可以 KILL 掉该事务的连接ID,但这会导致该事务执行的所有操作被回滚,需要评估业务影响。
        3. 配置监控:对 History List Length 和长事务设置监控告警,防患于未然。