MVCC,全称 Multi-Version Concurrency Control,是多版本并发控制的意思。
在高并发情况下操作数据库可能会出现脏写、脏读、不可重复度、幻读这四个问题。通过 MVCC 可以实现在不加锁的前提下避免一些问题。
首先,我们引入一个概念,即行数据的版本。每次通过事务对行数据进行更新的时候,都会生成一个新的数据版本,并记录事务的唯一 id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
下图记录了某一行数据的变更过程,图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
图中的虚线箭头,就是 undo log(回滚日志)。而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
一个事务对某条记录进行读操作时,会查看这一条记录的一系列事务 id(由 undo log 构成的版本链中的事务 id),并根据事务的隔离级别(“读已提交”、“可重复读”)去选择生成 Read View 的方式,通过比较事务 id 来确定可见的版本。
实现原理是:在事务创建的时候,InnoDB 会构建一个视图数组用来存储“活跃”的事务的 id。这里的“活跃”是指:启动了但还没有提交。
该视图数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
这个视图数组 + 高水位,就组成了当前事务的一致性视图(read-view)。
视图数组、低水位、高水位、当前事务 id 可记为:
是基于数据的 row trx_id 和这个一致性视图的对比结果,即可判断某一行数据的哪个版本是可见的。
数据版本可见性规则:
需要注意的是:上图的 id 并不是从左到右递增的,已提交的 id 可能会比活跃的 id 大。
不同隔离级别产生 Read View 的方式是不同的:
我们前面提到的实际上都是 MVCC 中的“快照读”,对应的就是最常见的不加锁的查询,如:
mysql> select k from t where id=1;
而事务中的 update 语句,使用的是“当前读。它用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
因此,在下面的例子中,事务 B 读到的 k = 3。
事实上,如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,返回的 k 的值也会是 3。下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;