深入解析MySQL隐藏字段DB_TRX_ID在MVCC版本链中的作用
DB_TRX_ID。本文直接剖析 DB_TRX_ID 在聚簇索引及 undo log 版本链中的定位,推导其与 ReadView 配合完成可见性判断的完整判定流程,并解析快照读与当前读中容易混淆的边界。
1. 聚簇索引记录的隐藏列与 DB_TRX_ID 物理定位
在 InnoDB 表中,每当向包含主键的聚簇索引插入一条记录时,物理数据页内保存的行格式除显式定义的用户列外,存储引擎还会强制追加两到三个隐式系统列:
- DB_TRX_ID(6字节):记录最后一次插入或修改该行记录的事务 ID(Transaction ID)。
- DB_ROLL_PTR(7字节):回滚指针,指向写入
undo log中的旧版本记录地址。 - DB_ROW_ID(6字节):如果表定义中没有显式主键且没有非空唯一索引,InnoDB 会隐式生成此自增行 ID 作为聚簇索引键。如果表已有显式主键,该列不会出现在行物理记录中。
DB_TRX_ID 并不是一个单纯用来“记录日志”的标记,而是整个并发版本控制系统的身份印鉴。每一个成功提交或正在活动的事务,在执行首次 INSERT、UPDATE 或 DELETE 时,都会向全局系统变量申请一个单调递增的事务 ID。这个 ID 会在事务修改数据行时,被物理写入当前聚簇索引行的 DB_TRX_ID 区域。
2. DB_TRX_ID 与 undo log 如何协同构造版本链
当一行数据被并发或连续修改多次时,InnoDB 不会在聚簇索引树上保存多个完整物理行,而是只在聚簇索引树上保留最新的一份物理记录。所有历史快照,都是借助 DB_ROLL_PTR 从数据页串联到 undo 页(回滚日志段)中的记录片段组合出来的。
当一次修改发生时,引擎的具体工作分为四步:
- 对当前行加排他锁(X锁),确保没有其他并发事务在同一时刻写入。
- 拷贝当前行数据写入 undo log:把该行原有的主键、
DB_TRX_ID、DB_ROLL_PTR以及被修改列的旧值打包成一条 undo 记录。 - 修改聚簇索引最新行:将聚簇索引中的列值更新为新值,将
DB_TRX_ID覆盖为当前事务的 ID。 - 更新回滚指针:将聚簇索引中的
DB_ROLL_PTR指向刚才写入 undo log 的那条历史版本。
由此,通过指针的单向链接,形成了一条从“最新物理记录”一路指向“最旧提交版本”的逻辑单向链表。在这条链上的每一个版本节点,均显式携带着生成它的 DB_TRX_ID。
3. 核心机制:ReadView 结构与 DB_TRX_ID 的可见性算法
有了版本链之后,读事务究竟能看到哪一个版本?这完全取决于 ReadView。
当事务执行快照读(普通的 SELECT)时,系统会生成一个 ReadView 快照结构。ReadView 内部维护了 4 个关键属性:
m_ids:在生成 ReadView 的那一瞬间,系统中所有活跃且未提交的读写事务 ID 列表。min_trx_id:生成 ReadView 时,当前活跃事务 ID 列表中的最小值(即m_ids中最小的值)。max_trx_id:生成 ReadView 时,系统即将分配给下一个事务的 ID(注意:是系统分配上限,不是m_ids的最大值)。creator_trx_id:创建该 ReadView 的事务自身的 ID。
当查询引擎沿着版本链检查某个版本的 DB_TRX_ID 时,会严格按照以下步骤进行四层比对判定:
4. 完整推演示例:RC 与 RR 隔离级别的表现差异
读已提交(Read Committed, RC)和可重复读(Repeatable Read, RR)底层的算法规则完全一致,唯一的本质区别在于生成 ReadView 的时机:
- RC 隔离级别:同一个事务内部,每一次执行普通的 SELECT 语句都会重新生成一份崭新的 ReadView。
- RR 隔离级别:事务开启后,仅在第一次执行快照读 SELECT 时生成一份 ReadView,随后该事务中的所有后续快照读都复用这同一份 ReadView,直到事务提交。
执行时间线与数据推导
假设存在如下初始记录:id = 1, balance = 100,该行历史写入事务已提交,初始 DB_TRX_ID = 80。
| 时间点 | 事务 A (ID: 100) | 事务 B (ID: 200) | 事务 C (纯读事务 / 查询者) |
|---|---|---|---|
| T1 | UPDATE t SET balance = 200 WHERE id = 1; |
- | - |
| T2 | - | - | 第一次查询:SELECT balance FROM t WHERE id = 1; |
| T3 | COMMIT; |
UPDATE t SET balance = 300 WHERE id = 1; |
- |
| T4 | - | COMMIT; |
- |
| T5 | - | - | 第二次查询:SELECT balance FROM t WHERE id = 1; |
在不同隔离级别下,事务 C 的判定过程如下:
场景 1:在可重复读(RR)模式下
-
T2 时刻第一次查询:事务 C 生成 ReadView。此时活跃事务为
m_ids = [100],min_trx_id = 100,max_trx_id = 201(假设系统最新分配 ID 为 200)。聚簇索引最新物理行的DB_TRX_ID = 100。因为 100 落在m_ids中,该版本记录对事务 C 不可见。事务 C 顺着DB_ROLL_PTR找到旧版本(DB_TRX_ID = 80),由于 80 小于min_trx_id,可见,查出balance = 100。 -
T5 时刻第二次查询:因为是 RR 模式,事务 C 复用 T2 时刻生成的 ReadView(此时
m_ids里依旧记录着当时的[100],max_trx_id = 201)。虽然此时事务 B 已经把聚簇索引物理行改成了balance = 300, DB_TRX_ID = 200,但因为 200 落在当时 ReadView 的m_ids活跃区间内;再顺着指针找到事务 A 改出的DB_TRX_ID = 100,依然在m_ids中不可见。最终再次回溯到DB_TRX_ID = 80的初始版本,依然返回balance = 100。这便杜绝了不可重复读。
场景 2:在读已提交(RC)模式下
- T2 时刻第一次查询:生成专属 ReadView,活跃列表为
[100],事务 A 尚未提交,返回balance = 100。 - T5 时刻第二次查询:重新生成全新的 ReadView。此时事务 A 和事务 B 都已提交,当前系统的活跃事务列表
m_ids = []。读取最新记录的DB_TRX_ID = 200,检查发现 200 不在m_ids且已提交,判定为可见,直接返回balance = 300。
5. 容易混淆的关键认知与技术辨析
辨析一:“DB_TRX_ID 会记录在每一个二级索引(Secondary Index)页面的行中吗?”
并不存在。很多初学者会误以为二级索引也像聚簇索引一样包含 DB_TRX_ID 和 DB_ROLL_PTR。实际上,InnoDB 二级索引的叶子节点记录中没有这两个隐藏列。
二级索引页的 Page Header 中只记录了一个 PAGE_MAX_TRX_ID(表示修改过该数据页的最大事务 ID)。当查询使用二级索引时:
- 如果该页面的
PAGE_MAX_TRX_ID小于当前 ReadView 的min_trx_id,说明这整个数据页上所有的修改在快照创建前就已全部提交,二级索引上的数据可以直接信任并读取,无需回表。 - 如果大于或等于,则说明当前页可能包含未提交或后续提交的数据,查询引擎必须回表到聚簇索引,通过聚簇索引上的
DB_TRX_ID和DB_ROLL_PTR沿版本链回溯真正的可见版本。
辨析二:“在事务中执行 SELECT ... FOR UPDATE 时,也会比对 DB_TRX_ID 找历史版本吗?”
不会。普通的 SELECT 是快照读(Snapshot Read),遵循 ReadView 与 DB_TRX_ID 的可见性判定规则。而 SELECT ... FOR UPDATE、UPDATE、DELETE 或 INSERT 属于当前读(Current Read)。
当前读会绕过版本链校验,直接对聚簇索引最新物理记录加行锁(Record Lock 或 Next-Key Lock),并读取最新已提交的值。如果当前读恰好遇到了未提交的事务,当前读会进入锁等待状态,而不是顺着 DB_ROLL_PTR 回退去读旧版本的 DB_TRX_ID。
在 RR 隔离级别下,如果事务 C 先快照读发现某记录不存在,随后执行了一条
UPDATE 刚好匹配到了其他并发事务已提交的这条新记录,本次 UPDATE 会触发当前读,将该行记录的 DB_TRX_ID 强制更新为事务 C 自己的 ID。接下来事务 C 再次执行快照读时,第一条规则(DB_TRX_ID == creator_trx_id)立即命中,导致这条原本看不到的记录突然变成了“自己修改的数据”而显现出来。理解 DB_TRX_ID 的写入机制是分析这类特殊案例的关键。
6. 总结与机制全貌
在整个 InnoDB 的多版本并发控制架构中:
- 物理载体:聚簇索引的每一行物理记录均携带
DB_TRX_ID,精确标明该行最近一次被谁修改。 - 逻辑链接:通过
DB_ROLL_PTR将修改前的旧数据在undo log中串成单向历史版本链。 - 裁判依据:当执行快照读时,查询事务基于当前系统的事务状态创建
ReadView。版本链上的每一个DB_TRX_ID就是唯一受审判的事务证件,通过与m_ids、min_trx_id、max_trx_id的对比,精确决定该版本数据是“直接可见”、“回溯读取更老版本”还是“纯属自身未提交改动”,最终在无须加锁的情况下达成了高并发的一致性视图。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









