MVCC (multiversion concurrency control),多版本并发控制,主要是经过在每一行记录中增长三个字段,与 undo log 中相关记录配合使用,同时加上可见性算法,使得各个事务能够在不加锁的状况下可以同时地读取到某行记录上的准确值(这个值对不一样的事务而言多是不一样的)。使用 MVCC,在不加锁的状况下也能读取到准确的数据,大大提升了并发效率。html
提到 MVCC,必须提到事务。关于事务,有四个特性,即咱们常说的 ACID。mysql
而事务隔离性又分为四种级别:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)、串行化(serializable)。算法
关于这四种隔离级别的差别,能够经过如下例子(例子来源于:林晓斌:MySQL实战45讲)来加以说明。sql
假设存在一张表,里面只有一个字段和一条记录,值是 1,如今发生如下的操做数据库
时刻 | 事务A |
事务B |
---|---|---|
t1 | 启动事务,查询获得值 1 | |
t2 | 启动事务 |
|
t3 | 查询获得值 1 | |
t4 | 将 1 改为 2 |
|
t5 | 查询获得值 V1 |
|
t6 | 提交事务 |
|
t7 | 查询获得值 V2 |
|
t8 | 提交事务 |
|
t9 | 查询获得值 V3 |
针对不一样的隔离级别,V一、V二、V3 读到的值不一样。数组
在「读未提交」的隔离级别下,因为 t4 时刻事务 B 将值改为了 2,虽然 B 还没提交事务,可是此时的修改对其余事务是可见的,因此 V一、V二、V3 查询到的值都是 2。并发
在「读提交」的隔离级别下,t4 时刻修改了值,可是在 t5 时刻,事务 B 尚未提交,此时事务 A 读取到的值仍是老的值,因此 V1 是 1,而在 t7 时刻,因为事务 B 已经在 t6 时刻提交了,此时事务 B 所作的修改对其余的事务均可见,因此事务 A 在 t7 时刻能看到事务 B 的修改,此时 V2 的值为 2,固然 V3 的值也为 2。mvc
在「可重复读」的隔离级别下,遵循 “事务在执行期间看到的数据必须是先后一致” 的要求,因此不管事务 B 是否修改值,也不管事务 B 是否提交,事务 A 在没提交前读到的值都是相同的,即 V1 和 V2 的值都是 1,当 A 事务提交后,再次查询时,事务 B 的修改就能被 A 看到了,因此 V3 的值为 2。spa
在「串行化」的隔离级别下,当事务 B 在 t4 时刻执行更新时,因为与事务 A 操做的是同一行,且出现读写冲突,此时事务 B 被会阻塞,等待事务 A 执行完毕后,再执行事务 B,因此 V1 和 V2 的值是 1,V3 的值是 2。3d
在数据库表的记录中,每个记录都会添加三个字段:
当对某个记录进行更新时,会将当前记录写入 undo log 中,并更新当前记录中 DBROLLPTR 字段值,使其指向刚才的 undo log record,而后更新当前记录相关字段值,同时更新 DBTRXID 字段,记录执行更新操做的事务 ID。简略的更新过程大体以下所示
由上面的更新操做能够得知,数据库表记录始终记录着最新的更新结果,那对于「可重复读」和「读提交」的隔离级别的事务,它是如何保证在开启本事务后,其余事务对记录进行了更新操做,而本事务仍然可以读取到准确的值(不是表记录的最新值,而是历史版本的值)的?从更新操做中能够得知,经过循环遍历 DBROLLPTR 能够拿到当前记录的历史版本(固然,只是活跃的事务,若是当前记录没有相关事务在操做,则会清理 undo log,就不能拿到历史版本数据了) 。可是这么多历史版本的数据,究竟哪一个版本的数据才是当前事务所要的呢?这时就要判断当前版本的数据是否对当前事务可见了。
在开启事务时,会将当前活跃的事务(已经开启了事务,可是尚未提交)的事务 ID 放在一个数组里面,同时记录数组里面最小的事务 ID 为「低水位」,记录当前系统已经建立的事务ID 的最大值加一为「高水位」。这三者组成了一个事务的一致性视图(read-view)。当事务要查询某个记录的数据时,实际上就是拿该记录的事务ID(包括历史版本的事务ID)和这个一致性视图进行比较,直到某个版本的数据是可见的为止。其查询过程以下:
其判断过程的流程图大体以下所示
关于判断数据可见性,除了上述用高水位、低水位和事务视图数组结合判断以外,能够简化成如下规则判断:
如今用一个例子(此例子来自:林晓斌:MySQL实战45讲)来对上述查找过程进行说明。假设在「可重复读」的隔离级别下,有如下的表结构和数据。
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);复制代码
假设进行如下的操做(事务C 的 update 操做完即自动提交事务),在进行如下操做前,假设当前活跃的事务 ID 为 99,记录(1,1)的 DBTRXID 值是 90。则事务 A 的视图数组是 [99, 100],事务 B 的视图数组是 [99, 100, 101],事务 C 的视图数组是 [99, 100, 101, 102]
事务A(事务ID:100) |
事务B(事务ID:101) |
事务C(事务ID:102) |
---|---|---|
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; |
当事务 A 执行查询语句时,其查询数据逻辑图(此图来自:林晓斌:MySQL实战45讲)以下所示
其查找过程以下,首先,获取记录的事务ID(101),比高水位大,不可见,因此取出记录的上一个历史版本,获取其事务ID(102),比高水位大,不可见,再获取记录的上一个历史版本,获取其事务ID(90),比低水位小,可见,因此返回这个记录中的 k 字段的值 1。
固然,也能够用简化版原本判断。过程以下,首先,获取记录(1,3),尚未提交,不可见,取出上一个历史版本(1,2),(1,2)已经提交,可是在本事务视图建立后提交的,不可见,继续取出上一个历史版本(1,1),(1,1)已经提交,且是在本事务视图建立前提交的,可见,因此最终返回 k 的值是 1。
此处须要额外关注的是,事务 B 的更新操做,是在当前记录的最新值上更新的,并非在历史数据上更新的,不然会丢失事务 B 的更新操做。其实,更新数据都是先读后写的,并且这个读,是读的当前值,称为“当前读”。
若是是在「读提交」的隔离级别下,处理逻辑相似,只是生成一致性视图的状况不一样:
因此上述例子,若是是在「读提交」隔离级别下,事务 A 在执行查询语句时,会建立新的一致性视图,此时一致性视图中的活跃事务ID数组是 [99, 100, 101],其查找过程以下,读取当前记录事务 ID(101),在视图数组中,不可见,取出上一个历史版本记录,读取事务ID(102),介于低水位和高水位之间,且不在视图数组中,可见,因此返回记录的 k 值 2。
[1] 林晓斌. 事务隔离:为何你改了我还看不见?[J/OL]. https://time.geekbang.org/column/article/68963 ,2018-11-19
[2] 林晓斌. 事务隔离:事务究竟是隔离的仍是不隔离的?[J/OL]. https://time.geekbang.org/column/article/70562 ,2018-11-30
[3] MySQL官方文档: https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html