简述 InnoDB 对 MVCC 的实现

一、简述

分为两个要点简述:

要点一:行记录的历史版本是什么样子的?
InnoDB 将行记录及其历史行记录通过隐藏字段(DATA_ROLL_PTR)链成一个链表。历史行记录其实就是 undo log,放在共享表空间的 undo 段。

要点二:当前事务进行快照读时,如何选择历史版本?
每个行记录及其历史行记录都有一个隐藏字段(DATA_TRX_ID),记录了最后更新该行记录的事务 ID。
事务的事务 ID 由 InnoDB 统一分配,越是后分配,事务 ID 越大,即事务 ID 是单增的。
RR 级别下,事务在第一个快照读时生成一个 read view。
RC 级别下,事务在每一次快照读都重新生成一次 read view。
每次事务进行快照读时,都会根据 read view,按照只能读取在创建 read view 之前已提交事务所做的修改的原则,选择最新的符合要求的历史行记录读取。

二、详述

2.1 undo log

同一行记录各个版本构成的链表,示意图(图片非原创):
简述 InnoDB 对 MVCC 的实现
事务每对行记录 update 一次,就会产生一个历史行记录。

一个可以加深理解的点是:行记录的 undo 链表,尾结点一定是某个事务对该记录的 insert 操作,中间结点都是事务对该记录的 update 操作,头结点可能是事务的 update 操作,也可能是事务的 delete 操作。

每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除,在 delete 行记录时,不会立即删除该记录,而是仅将 deleted_flag 置 1。当没有任何活跃事务的快照读需要依赖该记录时,该记录会被 InnoDB 的 purge 线程真正删除。

2.2 选择历史版本的具体实现

InnoDB 会维护一个所有当前活跃事务 ID 的列表。

当事务需要建立 read view 时:

  • 会拷贝一份 InnoDB 维护的活跃事务 ID 列表,记作 trx_ids。
  • 保存 InnoDB 准备分配的下一个事务 ID(大于当前所有事务 ID,包括已提交的事务),记作 low_limit_id。
  • 保存当前 read view 中最小的事务 ID,记作 up_limit_id。

有了这三样东西,再加上只能读取在创建 read view 之前提交事务所做的修改的原则,估计大家已经知道是怎么选择判断的了。

选择历史版本的流程:
从头结点依次向后判断,因为越在前面的记录越新,所以返回第一个符合要求的版本。
如果头结点就满足要求,还需要特判一下记录是否被删除,如果被删除,则返回空。

判断是否符合要求的流程:
取出该版本的事务 ID,判断该事务在创建 read view 时是否已提交。
拿该事务 ID 和 up_limit_id 比较,如果小于,则可以读取。
再拿该事务 ID 和 low_limit_id 比较,如果大于等于,说明该事务是创建 read view 后,新开启的事务,不可以读取。
最后判断该事务 ID 是否在 trx_ids 里,如果不在,则可以读取,否则不可以读取。