MySQL事务隔离原理(四种隔离级别的具体实现)

一、事务的定义

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

这里给出事务的一个比较正式的定义:

事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单元。

事务的开始与结束可以由用户显式控制。如果用户没有显式地定义事务,则由数据库管理系统默认规定自动划分事务。在SQL中,定义事务的语句一般有三条:

BEGIN TRANSACTION;
COMMIT;
ROLLBACK;

事务通常以BEGIN/START TRANSACTION开始,以COMMITROLLBACK结束。COMMIT表示提交,即提交事务的所有操作。具体地说就是将事务中所有对数据库的更新写回到磁盘的物理数据库中去,事务正常结束。ROLLBACK表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库所有已完成的操作全部撤销,回滚到事务开始时的状态。这里的操作指对数据库的更新操作。

需要注意的是,BEGIN TRANSACTION命令并不代表事务的开始,在执行到它之后的第一个操作InnoDB表的语句,事务才真正启动,例如下面示例中,select * from xxx 才是事务的开始

begin;
select * from xxx; 
commit; -- 或者 rollback;

如果我们想要马上启动一个事务,可以使用transaction with consistent snapshot

另外,可以通过以下语句来查询当前有多少事务正在运行:

select * from information_schema.innodb_trx;

二、事务的四个特性

具体见:MySQL事务的四大特性

三、事务的并发问题

MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。那么在同时处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。下面的例子说明了这些问题是如何发生的。

3.1 脏读

脏读指一个事务「读到」了另一个「未提交事务修改过的数据」。

脏读最大的问题就是可能会读到不存在的数据。比如在上图中,事务B的更新数据被事务A读取,但是事务B回滚了,更新数据全部还原,也就是说事务A刚刚读到的数据并没有存在于数据库中。从宏观来看,就是事务A读出了一条不存在的数据,这个问题是很严重的。

在这里插入图片描述

3.2 不可重复读

在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。

下图中事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致:
在这里插入图片描述

3.3 幻读

在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A修改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
在这里插入图片描述
注意不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

四、隔离性与隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,SQL 标准提出了四种隔离级别来规避这些现象。

在谈隔离级别之前,有一点需要申明,我们设置的隔离级别越高,那么事务并发执行的效率就会越低。因此很多时候,我们都要在隔离级别与效率之间寻找一个平衡点。SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable ),其解释如下:

  • 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。这是MySQL InnoDB 引擎的默认隔离级别;
  • 串行化(serializable ):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

下面用一个例子说明这几种隔离级别。假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为。

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

在这里插入图片描述

我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。

  • 若隔离级别是“读未提交”, 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
  • 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
  • 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  • 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。

五、事务隔离级别的实现

这里我们展开说明“可重复读”。

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

在这里插入图片描述

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

5.1 读未提交

在此隔离级别下,事务之间可以读取彼此未提交的数据。但注意在所有写操作执行时都会加排它锁,那还怎么读未提交呢?该级别主要的特点是释放锁的时机与众不同:在执行完写操作后立即释放,而不像其他隔离级别在事务提交以后释放。因此极易出现脏读(不可重复读和幻读就更不用说了)

但该级别的并发性能也正因为锁释放得很早而变得很高,就连写写操作都很难产生锁竞争。

5.2 读提交

读提交将锁的释放时机延迟到事务提交之后,解决了脏读。但是,锁的释放时机延迟了,不仅写与写操作之间会产生锁竞争,在锁释放之前,也无法执行读操作,这对并发性产生了很大的影响。为了提高并发性,MySQL采用了一种名为MVCC的解决方案:无视当前持有锁的事务,读取最新的历史版本数据。

(这里很奇怪,别的事务运行期间不是会修改吗,这个修改不会被看到?)
因此,在读提交的级别下,我们每次执行select操作时都会通过MVCC获取当前数据的最新快照,不加任何锁,也无视任何锁(因为历史数据是构造出来的,身上不可能有锁),完美解决读写之间的并发问题,和读未提交的并发性能只差在写写操作上。

而为了进一步提升写写操作上的并发性能,该级别下不会使用间隙锁,无论什么查询都只会加行锁,而且在执行完WHERE条件筛选之后,会立即释放掉不符合条件的行锁。

但是,正因为对并发性能的极致追求或者说贪婪,该级别下还是遗留了不可重复读和幻读问题:

  • MVCC版本的生成时机: 是每次select时,这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读
  • 锁的范围: 因为没有间隙锁,这就意味着,如果我们在事务A中多次执行select * from user where age>18 and age<30 for update时,其他事务是可以往age为(18,30)这个区间插入/删除数据的,那就出现了幻读

针对不同的隔离级别,并发事务时可能发生的现象也会不同:
在这里插入图片描述

所以,要解决脏读现象,就要升级到「读提交」以上的隔离级别;要解决不可重复读现象,就要升级到「可重复读」的隔离级别,要解决幻读现象不建议将隔离级别升级到「串行化」。

不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,有的数据库只实现了其中几种隔离级别,我们讨论的 MySQL 虽然支持 4 种隔离级别,但是与SQL 标准中规定的各级隔离级别允许发生的现象却有些出入。

MySQL 在「可重复读」隔离级别下(MySQL InnoDB引擎的默认隔离级别),可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。MySQL对于幻读的解决方案有两种:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

四、四种隔离级别的实现

4.1

4.3 可重复读

既然读提交依然有较大的数据可靠性能问题,那我们可以进一步进行限制,这样就得到了可重复读,该级别在读提交的基础上做了两点修改,从而避免了不可重复读和幻读:

  • MVCC版本的生成时间: 一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读
  • 锁的范围: 在行锁的基础上,加上Gap Lock,从而形成Next-Key Lock,在所有遍历过的(不管是否匹配条件)索引行上以及之间的区域上,都加上锁,阻塞其他事务在遍历范围内进行写操作,从而避免了幻读

尽管InnoDB在可重复读级别下已经将数据可靠性和并发性能两方面做得尽善尽美了,但前提是用户查询时能够主动善用Locking Reads,即select … lock in share mode和select … for update。如果只是使用普通的select,依然防不住幻读。

这是因为MVCC的快照只对读操作有效,对写操作无效,举例说明会更清晰一点: 事务A依次执行如下3条sql,事务B在语句1和2之间,插入10条age=20的记录,事务A就幻读了:

1\. select count(1) from user where age=20;
-- return 0: 当前没有age=20的
2\. update user set name=test where age=20;
-- Affects 10 rows: 因为事务B刚写入10条age=20的记录,而写操作是不受MVCC影响,能看到最新数据的,所以更新成功,而一旦操作成功,这些被操作的数据就会对当前事务可见
3\. select count(1) from user where age=20;
-- return 10: 出现幻读  

这种场景,需要用户主动使用Locking Read来防止其他事务在查询范围内进行写操作,因此,为了防患于未然,隔离级别又往前迈了一步

4.4 串行化

该级别下,会自动将所有普通select转化为select … lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低.

可重复读级别下使用Locking Read也可以变成读写互斥,那这两个有什么区别呢?可重复读我们可以自己选择是否使用Locking Read,更自由,所以在可重复读下我们可以选择使用普通的select读写实现并发。

4.5 小结

在这里对介绍的四种隔离级别的实现做一个小结,“读未提交”隔离级别下直接返回记录上的最新值,没有MVCC视图概念;在“读提交”和“可重复度读“隔离级别下,数据库里面会创建一个MVCC视图,访问的时候以视图的逻辑结果为准,在”读提交“级别下,这个MVCC视图是在每个 SQL 语句开始执行的时候创建的;在“可重复读”隔离级别下,这个MVCC视图是在事务启动时创建的,整个事务存在期间都用这个视图;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

五、以可重复读的实现为例介绍MVCC

5.1 MVCC的工作原理

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录:
在这里插入图片描述
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。

我们可以发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。所以使用多版本并发控制(Multi-Version Concurrency Control, MVCC),可以有效地提升数据库并发能力(乐观锁的一种实现方式),常见的数据库如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。

如果没有MVCC,当想要读取的数据被其他事务用排它锁锁住时,只能互斥等待;而这时MVCC可以通过提供历史版本从而实现读取被锁的数据(的历史版本),这时只有写写之间相互阻塞,其他三种操作(读写、写读、读读)都可以并行。

在 MySQL中,多版本并发控制是 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现读已提交和可重复读这两种隔离级别。而读未提交隔离级别总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

MVCC一般有两种实现方式,本文所讲的InnoDB采用的是后者:

  • 实时保留数据的一个或多个历史版本
  • 在需要时通过undo日志构造出历史版本

5.2 MVCC的具体实现

首先有一些特殊定义的数据结构需要我们了解:

5.2.1 隐藏字段

InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:

  1. DB_TRX_ID(6字节):在InnoDB中,每个事务都有一个唯一的事务ID,叫做transaction id(缩写trx_id),它是在事务开始时候向InnoDB的事务系统申请的,并且按照申请顺序严格递增。 在这里DB_TRX_ID就表示最近一次对该行数据作修改(insert或update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted,并非真正删除。
  2. DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息
  3. DB_ROW_ID(6字节):这是随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,InnoDB就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个字段和MVCC关系不大,所以我们在这里不必关注。

5.2.2 Read View(一致性视图)

这里对Read View做更深一步的剖析,在上面介绍时我们对read view的理解可能是保存在事务内部的对数据库的一个备份,但是其实read view的真正作用是用来做可见性判断的,里面保存了“对本事务不可见的其他活跃事务”。

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。当然,如果“上一个版本”也不可见,那就得继续往前找。

在可见性的实现上,InnoDB为每个事务构建了一个数组,用来保存这个事务启动瞬间,当前正在”活跃“的所有事务ID。”活跃“指的是启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。这里需要注意:低水位到高水位之间的某些事务ID是没在数组中的,没在的原因是它们已经提交了,比如低水位为100,高水位为106,而数组中可能只有100、101、103、105这四个事务ID,104和102不在的原因是因为在当前事务启动时,这两个事务已经提交了。

在低水位之前的,很明显是已经提交了的事务,而在高水位之后的,则是当前事务启动之后才开始的,因此高低水位把所有的DB_TRX_ID分成了几种不同的情况:
在这里插入图片描述
这样,对于当前事务的启动瞬间来讲,一个数据的DB_TRX_ID,存在以下几种可能:

  • 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  • 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  • 如果落在黄色部分,那就包括两种情况:
    a. 如果DB_TRX_ID在数组中(也就说明这个事务在当前事务启动时还活跃),那么表示这个这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

一致性视图的代码结构如下:

struct read_view_t{
	ulint								type;
	undo_no_t							undo_no;
	trx_id_t							low_limit_no;
	trx_id_t							low_limit_id;
	trx_id_t							up_limit_id;	
	ulint								n_trx_ids;
	trx_id_t*							trx_ids;
	trx_id_t							creator_trx_id;
	UT_LIST_NODE_T(read_view_t)			view_list;
}

我们着重关注的是这三个变量:

  • trx_ids:这个就是一致性视图创建时其他未提交的活跃事务ID列表,也就是我们上面提到的数组。意思就是创建read view时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。(注意不包括自己和已提交的事务)
  • low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID,也即上面提到的高水位。
  • up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id为low_limit_id,也即上面提到的低水位。

在这里也提下读提交和可重复读的read view产生区别:

  1. 在innodb中的可重复读级别, 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务之后都是使用的这个快照,不会重新创建,直到事务结束。
  2. 在innodb中的读提交级别, 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。

5.2.3 undo log

undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本,这也是InnoDB利用”所有数据都有多个版本“这个特性,来实现可见性的核心。

下图记录了一行数据被多个事务连续更新后的状态(图中的row trx_id就是上面提到的DB_TRX_ID):
在这里插入图片描述
图中虚线框内是同一行数据的四个版本,当前最新版本是 V4,k 的值是 22,它是被 事务ID 为 25 的事务更新的,因此它的 DB_TRX_ID 是 25。

在上图中,三个虚线箭头其实就代表了undo log;V1、V2、V3其实并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的,比如,需要V2的时候,就是通过V4依次执行U3、U2计算出来。

比如,假如有一个事务的低水位是18,它要读取上面图中的数据,那么当它访问时候,获取了当前的DB_TRX_ID为25,假设这个25在数组中(说明这个25在事务启动时依然活跃),那么因为25高于低水位,所以对于当前事务来说不可见,于是这个事务就会从V4通过U3计算得出V3,V3的DB_TRX_ID=17小于18,所以这个数据是可见的,所以对于当前事务来讲,这个事务的值通过undo log就可以构造出来,为11。

大多数对数据的变更操作包含insert/update/delete,在InnoDB里,undo log分为如下两类:

  1. insert undo log:事务insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后就可以立即丢弃
  2. update undo log:事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照不涉及该日志记录,对应的回滚日志才会被purge线程删除。

Purge线程: 为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

5.3 记录行修改的具体流程

假设有一条记录行如下,字段有Name和Honor,值分别为curry和mvp,最新修改这条记录的事务ID为1。
在这里插入图片描述

5.3.1 现在事务A(事务ID为2)对该记录的Honor做出了修改,将Honor改为fmvp

  1. 事务A先对该行加排他锁
  2. 然后把该行数据拷贝到undo log中,作为旧版本
  3. 拷贝完毕后,修改该行的Honor为fmvp,并且修改DB_TRX_ID为2(事务A的ID),回滚指针指向拷贝到undo log的旧版本(然后还会将修改后的最新数据写入redo log)
  4. 事务提交,释放排他锁

在这里插入图片描述

5.3.2 接着事务B(事务ID为3)修改同一个记录行,将Name修改为iguodala

  1. 事务B先对该行加排他锁
  2. 然后把该行数据拷贝到undo log中,作为旧版本
  3. 拷贝完毕后,修改该行Name为iguodala,并且修改DB_TRX_ID为3(事务B的ID),回滚指针指向拷贝到undo log最新的旧版本
  4. 事务提交,释放排他锁

在这里插入图片描述
从上面可以看出,不同事务或者相同的事务对同一记录行的修改,会使该记录行的undo log成为一条链条,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。

5.4 可见性比较算法

在InnoDB中,创建一个新事务后,执行第一个select语句的时候,InnoDB会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务ID列表,即(trx_ids)。当用户在这个事务中读取某个记录行的时候,InnoDB会将该记录行的DB_TRX_ID与该read view中的一些变量进行比较,判断是否满足可见性条件。

假设当前事务要读取某一个记录行,该记录行的DB_TRX_ID(即最新修改该行的事务ID)为trx_id,read view的活跃事务列表trx_ids中最早的事务ID为up_limit_id,将在生成这个read view时系统出现过的最大的事务ID记为low_limit_id(即还未分配的事务ID)

具体的比较算法如下:

  1. 如果trx_id<up_limit_id,那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的,跳到步骤5.
  2. 如果trx_i>=low_limit_id,那么表明“最新修改该行的事务”在“当前事务”创建快照之后才修改该行,所以该记录行的值对当前事务不可见,跳到步骤四
  3. 如果up_limit_id<=trx_id<low_limit_id,表明“最新修改改行的事务”在“当前事务”创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表trx_ids进行查找(源码中使用的是二分查找,因为是有序的):
    (1)如果在活跃事务列表trx_ids中能找到id为trx_id的事务,表明或者在“当前事务”创建快照前,“该记录行的值”被“id为trx_id的事务”修改了,但没有提交,或者在“当前事务”创建快照后,“该记录行的值”被"id为trx_id的事务“修改了(不管有无提交);在这些情况下,这个记录行的值对当前事务都是不可见的,跳到步骤四
    (2)在活跃事务列表中找不到,则表明"id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,跳到步骤5。
  4. 在该记录行的DB_ROLL_PTR指针所指向的undo log回滚段中,取出最新的旧事务号DB_TRX_ID,将它赋给trx_id,然后跳到步骤1重新开始判断
  5. 将该可见行的值返回

5.5 当前读和快照读

快照读(snapshot read):普通的select语句(不包括select… lock in share mode, select … for update)
当前读(current read):select … lock in share mode, select … for update,insert,update,delete语句(这些语句获取的是数据库中的最新数据)

只靠MVCC实现可重复读隔离级别,可以保证可重复读,还能防止部分幻读,但并不是完全防止。

比如事务A开始后,执行普通select语句,创建了快照;之后事务B执行insert语句;然后事务A再执行普通select语句,得到的还是之前B没有insert过的数据,因为这时候A读的数据是符合快照可见性条件的数据。这就防止了部分幻读,此时事务A是快照读。

但是,如果事务A执行的不是普通select语句,而是select … for update等语句,这时候,事务A是当前读,每次语句执行的时候都是获取的最新数据。也就是说,在只有MVCC时,A先执行 select … where nid between 1 and 10 … for update;然后事务B再执行 insert … nid = 5 …;然后 A 再执行 select … where nid between 1 and 10 … for update,就会发现,多了一条B insert进去的记录。这就产生幻读了,所以单独靠MVCC并不能完全防止幻读。

因此,InnoDB在实现RR隔离级别时,不仅使用了MVCC,还会对“当前读语句”读取的记录行加记录锁(record lock)和间隙锁(gap lock),禁止其他事务在间隙间插入记录行,来防止幻读。也就是前文说的"行级锁+MVCC"。

5.6 例子

假设原始数据行

FieldDB_ROW_IDDB_TRX_IDDB_ROLL_PTR
010100000x13525342

例1

在这里插入图片描述

MVCC参考一
MVCC参考二

  • 30
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值