版本链
InnoDB的聚簇索引记录中都包含两个必要的隐藏列:
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本)。
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。
ReadView
每行数据有多个版本,每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本,每个版本有自己的 row trx_id。
语句更新会生成 undo log(回滚日志),图中的三个虚线箭头,就是 undo log。 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
事务能看到的数据,是生成快照的那个版本,以及这个事务自己更新的数据。
实现
InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在活跃的所有事务 ID。
数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1记为高水位。 这个视图数组和高水位,就组成了当前事务的一致性视图。
数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。
ReadView 中主要包含4个比较重要的内容:
- m_ids:生成 ReadView 时当前系统中活跃的读写事务的事务id列表(活跃事务数组)。
- min_trx_id:生成 ReadView 时当前系统中活跃的读写事务中最小的事务id,也就是 m_ids 中的最小值。
- max_trx_id:生成 ReadView 时系统中应该分配给下一个事务的 id 值(创建过的事务 ID 的最大值加 1)。
- creator_trx_id:生成该 ReadView 的事务的事务 id 。
在访问某条记录时,判断记录的某个版本是否可见:
- 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问(当前事务自己修改的)。
- 如果被访问版本的 trx_id 属性值小于ReadView中的 min_trx_id 值,表明生成该版本的事务在当前事务生成ReadView 前已经提交,所以该版本可以被当前事务访问(绿色部分)。
- 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的max_trx_id值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问(红色部分)。
- 如果被访问版本的 trx_id 属性值在ReadView的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中(黄色)。
- 如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见。
- 版本已提交,但是是在视图创建后提交的,不可见。
- 版本已提交,而且是在视图创建前提交的,可见。