接着,id = 300 的事务 C 再次修改了这条行记录,把 age 从 20 修改成了 30,如下图:

可以看到,每次修改行记录都会更新 trx_id 和 roll_pointer 这两个隐藏字段,之前的多个数据快照对应的 undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链。
需要注意的是,select 查询操作不会生成 undo log!在 InnoDB 存储引擎中,undo log 只分为两种:
-
insert undo log:在 insert 操作中产生的 undo log
-
update undo log:对 delete 和 update 操作产生的 undo log
事实上,由于事务隔离性的要求,insert 操作的记录,只对事务本身可见,对其他事务不可见,也即插入操作不会对已经存在的记录产生影响!,所以也就不存在并发情况下的问题。所以,也就是说,MVCC 这个机制,其实就是靠 update undo log 实现的,和 insert undo log 基本上没啥关系,我们上面说的 undo log 版本链上的其实就是 update undo log。
ReadView 机制
说到 MVCC,说到 undo log 版本链,如果你自己不往下说的话,八九不离十面试官都会问你下 ReadView 这个机制。
咱也不卖官子,直接说吧,ReadView 机制就是用来判断当前事务能够看见哪些版本的,一个 ReadView 主要包含如下几个部分:
-
m_ids:生成 ReadView 时有哪些事务在执行但是还没提交的(称为 ”活跃事务“),这些活跃事务的 id 就存在这个字段里 -
min_trx_id:m_ids 里最小的值 -
max_trx_id:生成 ReadView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务 ID 越大) -
creator_trx_id:当前创建 ReadView 事务的 ID
接下来,再掏出 user 表,通过一个例子来理解下 ReaView 机制是如何做到判断当前事务能够看见哪些版本的:
假设表中已经被之前的事务 A(id = 100)插入了一条行记录(id = 1, username = "J
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
ack", age = 18),如图所示:

接下来,有两个事务 B(id = 200) 和 C(id = 300)过来并发执行,事务 B 想要更新(update)这行 id = 1 的记录,而事务 C(select)想要查询这行数据,这两个事务都执行了相应的操作但是还没有进行提交:

如果现在事务 B 开启了一个 ReadView,在这个 ReadView 里面:
-
m_ids就包含了当前的活跃事务的 id,即事务 B 和事务 C 这两个 id,200 和 300 -
min_trx_id就是 200 -
max_trx_id是下一个能够分配的事务的 id,那就是 301 -
creator_trx_id是当前创建 ReadView 事务 B 的 id 200

现在事务 B 进行第一次查询(上面说过 select 操作不会生成 undo log 的哈),会把这行记录的隐藏字段 trx_id 和 ReadView 的 min_trx_id 进行下判断,此时,发现 trx_id 是 100,小于 ReadView 里的 min_trx_id(200),这说明在事务 B 开始之前,修改这行记录的事务 A 已经提交了,所以开始于事务 A 提交之后的事务 B、是可以查到事务 A 对这行记录的更新的。
row.trx_id < ReadView.min_trx_id

接着事务 C 过来修改这行记录,把 age = 18 改成了 age = 20,所以这行记录的 trx_id 就变成了 300,同时 roll_pointer 指向了事务 C 修改之前生成的 undo log:

那这个时候事务 B 再次进行查询操作,会发现这行记录的 trx_id(300)大于 ReadView 的 min_trx_id(200),并且小于 max_trx_id(301)。
row.trx_id > ReadView.min_trx_id && row.trx_id < max_trx_id
这说明一个问题,就是更新这行记录的事务很有可能也存在于 ReadView 的 m_ids(活跃事务)中。所以事务 B 会去判断下 ReadView 的 m_ids 里面是否存在 trx_id = 300 的事务,显然是存在的,这就表示这个 id = 300 的事务是跟自己(事务 B)在同一时间段并发执行的事务,也就说明这行 age = 20 的记录事务 B 是不能查询到的。

既然无法查询,那该咋整?事务 B 这次的查询操作能够查到啥呢?
没错,undo log 版本链!
这时事务 B 就会顺着这行记录的 roll_pointer 指针往下找,就会找到最近的一条 trx_id = 100 的 undo log,而自己的 id 是 200,即说明这个 trx_id = 100 的 undo log 版本必然是在事务 B 开启之前就已经提交的了。所以事务 B 的这次查询操作读到的就是这个版本的数据即 age = 18。
通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务不会读到并发执行的另一个事务的更新。
那自己修改的值,自己能不能读到呢?
这当然是废话,肯定可以读到呀。不过上面的例子我们只涉及到了 ReadView 中的前三个字段,而 creator_trx_id 就与自己读自己的修改有关,所以这里还是图解出来让大家更进一步理解下 ReadView 机制:
假设事务 C 的修改已经提交了,然后事务 B 更新了这行记录,把 age = 20 改成了 age = 66,如下图所示:

然后,事务 B 再来查询这条记录,发现 trx_id = 200 与 ReadView 里的 creator_trx_id = 200 一样,这就说明这是我自己刚刚修改的啊,当然可以被查询到。
row.trx_id = ReadView.creator_trx_id

那如果在事务 B 的执行期间,突然开了一个 id = 400 的事务 D,然后更新了这行记录的 age = 88 并且还提交了,然后事务 B 再去读这行记录,能读到吗?

答案是不能的。
