mysql MVCC 介绍

简介

MVCC (multiversion concurrency control),多版本并发控制,主要是经过在每一行记录中增长三个字段,与 undo log 中相关记录配合使用,同时加上可见性算法,使得各个事务能够在不加锁的状况下可以同时地读取到某行记录上的准确值(这个值对不一样的事务而言多是不一样的)。使用 MVCC,在不加锁的状况下也能读取到准确的数据,大大提升了并发效率。html

事务

提到 MVCC,必须提到事务。关于事务,有四个特性,即咱们常说的 ACID。mysql

  • 原子性(Atomicity):表示事务要么所有执行,要么所有不执行,这是一个不可分割的最小单元
  • 一致性(Consistency):表示事务老是从一个一致的状态转移到另外一个一致的状态
  • 隔离性(Isolation):表示各个事务之间相关隔离,互不影响
  • 持久性(Durability):指一个事务一旦被提交,它对数据库的改变就是永久性的,即便后续数据库发生故障也不会有影响

而事务隔离性又分为四种级别:读未提交(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

MVCC

更新操做

在数据库表的记录中,每个记录都会添加三个字段:

  • DBTRXID:6个字节,表示最近一次修改本记录的事务ID
  • DBROLLPTR :7 个字节,回滚指针,指向回滚段中的 undo log record,用于找出这个记录的上个修改版本的数据。
  • DBROWID:6 个字节,一个单调递增的 ID,肯定表中记录的惟一性。

当对某个记录进行更新时,会将当前记录写入 undo log 中,并更新当前记录中 DBROLLPTR 字段值,使其指向刚才的 undo log record,而后更新当前记录相关字段值,同时更新 DBTRXID 字段,记录执行更新操做的事务 ID。简略的更新过程大体以下所示

mysql-mvcc-update

查询操做

由上面的更新操做能够得知,数据库表记录始终记录着最新的更新结果,那对于「可重复读」和「读提交」的隔离级别的事务,它是如何保证在开启本事务后,其余事务对记录进行了更新操做,而本事务仍然可以读取到准确的值(不是表记录的最新值,而是历史版本的值)的?从更新操做中能够得知,经过循环遍历 DBROLLPTR 能够拿到当前记录的历史版本(固然,只是活跃的事务,若是当前记录没有相关事务在操做,则会清理 undo log,就不能拿到历史版本数据了) 。可是这么多历史版本的数据,究竟哪一个版本的数据才是当前事务所要的呢?这时就要判断当前版本的数据是否对当前事务可见了。

在开启事务时,会将当前活跃的事务(已经开启了事务,可是尚未提交)的事务 ID 放在一个数组里面,同时记录数组里面最小的事务 ID 为「低水位」,记录当前系统已经建立的事务ID 的最大值加一为「高水位」。这三者组成了一个事务的一致性视图(read-view)。当事务要查询某个记录的数据时,实际上就是拿该记录的事务ID(包括历史版本的事务ID)和这个一致性视图进行比较,直到某个版本的数据是可见的为止。其查询过程以下:

  • 读取的记录的事务ID小于低水位,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据
  • 读取的记录的事务ID大于高水位,说明这个版本的数据在开启本事务后提交的,不可见,从记录中取出 DBROLLPTR 指向的记录并读取其事务 ID,开始下一轮的判断
  • 读取的记录的事务ID介于低水位和高水位中间,此时判断事务ID是否在一致性视图的事务数组中:
    • 若是不在,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据
    • 若是在,说明这个版本的数据是由开启事务后的其余活跃事务提交的,对本事务是不可见的,所以须要从记录中取出 DBROLLPTR 指向的记录并读取其事务 ID,开始下一轮的判断

其判断过程的流程图大体以下所示file

关于判断数据可见性,除了上述用高水位、低水位和事务视图数组结合判断以外,能够简化成如下规则判断:

  • 对于当前事务中的数据,可见
  • 对于其余事务中的数据
    • 若是版本未提交,不可见
    • 若是版本已经提交,且是在建立本事务视图后提交的,不可见
    • 若是版本已经提交,且是在建立本事务视图前提交的,可见

例子

如今用一个例子(此例子来自:林晓斌: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讲)以下所示

mysql-mvcc-query

其查找过程以下,首先,获取记录的事务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。

其余

  • 四种隔离级别,只有「读提交」和「可重复度」两个隔离级别可以使用 MVCC,所以也只有这两个隔离级别会建立一致性视图(read-view)。由于「读提交」隔离级别下每次都是读取的最新记录,因此不用 MVCC,也不用建立一致性视图;「串行化」隔离级别,则是用加锁方式来实现并发的,也不用 MVCC ,因此也不用建立一致性视图。关于「可重复度」和「读提交」两个隔离级别下一致性视图的差异,主要体如今:「可重复度」隔离级别下的一致性视图是在启动事务时建立的,建立后,本事务共用一个视图;而「可读提交」隔离级别下的一致性视图是在执行 SQL 时建立的,每个 SQL 都会单首创建一个视图,并不会共用。
  • 当前读(current read),每次读取的都是记录的最新数据,主要包含如下 SQL 语句
    • select ... lock in share mode
    • select ... for update
    • insert
    • update
    • delete
  • 快照读(snapshot read),可能读取记录的历史版本数据,主要用于 MVCC 中的简单的 select (不包括 select ... lock in share mode,select ... for update),保证事务读取的一致性。

参考资料

[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

相关文章
相关标签/搜索