MVCC分析
MySQL默认的事务隔离级别是可重复读,在事务开始时获得一致性视图,但更新数据时需要拿到行锁,如果与其他事务出现冲突需要进行等待,等待后获得锁再进行更新操作时读到的值是最新值还是创建一致性视图时的值呢?下面我们通过一个实验来验证。
下面为一张表的创建语句:
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
然后我们分别开启三个事务
事务A | 事务B | 事务C |
---|---|---|
start transaction with consistent snapshot | ||
start transaction with consistent snapshot | ||
update t set k = k+1 where id = 1; | ||
update t set k = k+1 where id = 1;select k from t where id = 1; | ||
select k from t where id = 1;commit | ||
commit; |
注意,事务启动并非是start transition
,而是执行到第一个InnoDB操作语句,上面的实验结果为:事务A查询到的值为1,事务B查询到的值为3。下面将详细解释一下原因。
首先MySQL的事务隔离通过MVCC来实现,每个事务在启动的时候会得到一个严格递增的事务ID: transaction id
,而在保存数据时,每行数据都有多个版本,每次更新时并不覆盖原始数据而是生成多个版本,版本之间通过row trx_id
进行区别,row trx_id
的值就是更新的事务的transaction id
(并不是真的保存那么多行,而是用的时候通过undo log恢复)
然后,在每个事务启动时,会用一个数组来记录下这个瞬间所有活跃事务的ID(启但没提交),将这组ID中的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位,然后将所有的row trx_id
分成三类:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
a. 若row trx_id
在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若row trx_id
不在数组中,表示这个版本是已经提交了的事务生成的,可见
下面分析一下上面的ABC事务执行的问题
不妨设事务A,B,C的ID分别为100,101,102,则有:
如图所示,对于事务A来说,它的transition id
为100,所以只能看到90时候的数据,所以k=1,而对于事务C来说,也毫无疑问应该看到k=2;唯一比较奇怪的是对于事务B,由于事务B是一个更新操作,对于所有的更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的row trx_id是101,当之后执行事务B的查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的k的值是3。
对于当前读来说,除了update
,也可以通过select + lock in share mode 或 for update
来实现,注意当前读是需要加锁的,如果有事务正在修改这一行,那么当前事务需要等待。
参考文献
MySQL实战45讲