MVCC
在本篇内容中主要探讨MVCC中的版本链选取问题。在讨论MVCC底层的版本链前,先复现一个问题。
一、问题复现
1、修改主键id的现象
背景:在MySQL中有一张如下图的表,id是自增主键,name和money是普通的字段,隔离级别是默认的可重复读。 account表的数据一开始是这样的:
A、B是两个不同的事务,可以保证的是,他们一开始的快照的一致的。然后分别进行如下顺序操作:
事务A | 事务B |
---|
begin; | begin; | select * from account; (1) | select * from account; (2) | | update account set id = 15 where id = 14; | | select * from account; (3) | select * from account; (4) | | | commit; | select * from account; (5) | | update account set id = 16 where id = 15; | | select * from account; (6) | | commit; | | select * from account; (7) | |
如果现在让你去猜测每次select后的结果,你是否能全部答对呢?
现在揭晓一下答案吧-------------------------------------------------------------
-
(1)、(2)、(4)、(5)的结果是一致的,数据和一开始的快照一模一样。 -
(3) 可以看到自己事务B的更改,将id = 14 变更为 id = 15,也没有问题吧。 -
到(6)这块儿就有点奇怪了,先不说select到什么数据,单这个update 能够成功吗,要知道(5) select到的可没有id = 15这条数据。 答案是 可以执行成功的!!!,这就不得不说起 MVCC 针对的是 select 而提供的 快照读(事务开始前的快照数据),而 update 、insert、delete、select … for update、select lock in share mode 等语句使用的 当前读 (事务最新提交的数据)。 按照上表的执行顺序,在事务A执行 update 的时候,事务B已经执行过 update 并已提交。所以事务A在 update 的时候是能感知到 id = 15这条数据存在的。 看到这个执行结果是不是又有点疑惑,id = 14已经被事务B给变更到了,为什么事务A依然能select到呢?留着这个疑问继续下去… -
(7) 的结果是不是好像对,又好像不对,id = 14 没有了,又恢复成你理解的样子了。
2、修改普通字段的现象
这里只需要对表里的普通字段按照之前的顺序也改一下(建议数据是唯一的,比较方便看到效果),不会呈现出上述的那些问题,一切都是你理解的样子。
二、解释问题
1、版本链
这里不得不提到 MySQL的 undo log ,了解MVCC的人都知道,它所提供的快照版本是由 undo log 文件提供的。同时 undo log 和 redo log 是有关联的,undo 负责事务的数据回滚,redo 负责事务的数据记录。
同一个事务对同一条数据进行多次修改或者多个事务对同一条数据进行修改,这些修改都会按照时间顺序连成一条链,MVCC可以通过这条链来查找这份数据的历史变更。 对于版本链的每个节点中,会记录着DB_ROW_ID(主键)、DB_TRX_ID(事务ID)、DB_ROLL_PTR(下一历史节点指针)以及对应的数据等。
2、可见性规则
在事务生成快照读的时候,会根据一定的算法来给出对应快照。 该算法会给出三个数据,分别是 活跃事务的数组ID、低水位、高水位,其中低水位是由数组中的最小值推导的,而高水位由当前最大事务ID + 1生成的。
如上图所示,分为三块区域:
有了这些可见性原则,我们就可以沿着一条数据的版本链来对比 事务id 和 数据的删除状态来判断是否可见了。
3、解释疑惑
事务A | 事务B |
---|
begin; | begin; | select * from account; (1) | select * from account; (2) | | update account set id = 15 where id = 14; | | select * from account; (3) | select * from account; (4) | | | commit; | select * from account; (5) | | update account set id = 16 where id = 15; | | select * from account; (6) | | commit; | | select * from account; (7) | |
之前提到(6)的时候,数据如下: 事务B修改 id = 14 为 id = 15 并提交事务后,当事务A再次读取数据时,事务B对 id = 14的修改根据可见性原则(如果事务B后于事务A开启,那么它属于高水位,不可见;如果事务B先于事务A开启,那么它属于活跃事务,也是不可见的)并不可见,所以事务会沿着版本链继续读取之前的事务的变更,符合可见性原则后,呈现出 id = 14这条数据。而 id = 16是由于 update 语句是当前读,并且属于自身操作,是可见的。
而(7)的数据如下: (7)已经是在事务A提交后读取的,所以在它读取时,之前事务A和事务B的一切数据都是可见的,所以它只需要根据数据是否处于被删除状态忽略id = 14 ,从而读取到 id = 12 和 id = 16.。
这里有意思的情况是,对主键的 update,相当于删除了一条数据,并且插入了一条新数据!!!
|