杰哥教你 MVCC,你听吗


在讲解 MVCC 之前,我们先简单来复习一下事务的四大特性(ACID):

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全
    不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独
    立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应
    该对其有任何影响。

今天,我们讲解的就是隔离性 🤩🤩🤩

一、事务隔离级别

我们为啥要有事务的隔离级别呢,说白了,就是解决事务并发执行时遇到的问题,那事务并发执行到底会产生哪些问题呢?下面让我们来康康。

1、脏写( Dirty Write )

脏写:如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写,我们还是以实际的例子来做说明:

假如我们这里有一个用户表:

num name age
1 彬彬 20

并且有如下会话:

发生时间编号 Session A Session B
1 BEGIN
2 BEGIN
3 update user set name = ‘杰哥’ where num = 1
4 update user set name = ‘阿伟’ where num = 1
5 COMMIT
6 ROLLBACK

在上表中,Session A 和 Session B 各开启了一个事务, Session B 中的事务先将 num 列为 1 的记录的 name 列更新为 ‘杰哥’ ,然后 Session A 中的事务接着又把这条 num 列为 1 的记录的 name 列更新为’阿伟‘ 。如果之后 Session B 中的事务进行了回滚,那么 Session A 中的更新也将不复存在,这种现象就称之为脏写 。

2、脏读( Dirty Read )

脏读:如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了 脏读

发生时间编号 Session A Session B
1 BEGIN
2 BEGIN
3 update user set name = ‘杰哥’ where num = 1
4 select * from user where num = 1
5 COMMIT
6 ROLLBACK

在上表中, Session A 和 Session B 各开启了一个事务, Session B 中的事务先将 num 列为 1 的记录的 name 列更新为 ‘杰哥’ ,然后 Session A 中的事务再去查询这条 num 为 1 的记录,如果读到列 name 的值为 ‘杰哥’ ,而 Session B 中的事务稍后进行了回滚,那么 Session A 中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读 。

3、不可重复读(Non-Repeatable Read)

不可重复读:如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读。

发生时间编号 Session A Session B
1 BEGIN
2 select * from user where num = 1
3 update user set name = ‘杰哥’ where num = 1
4 select * from user where num = 1
5 update user set name = ‘阿伟’ where num = 1
6 select * from user where num = 1

如上表:我们在 Session B 中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了 num 列为 1 的记录的列 name 的值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为不可重复读 。

4、幻读(Phantom)

幻读:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读。

发生时间编号 Session A Session B
1 BEGIN
2 select * from user where num > 1
3 insert into user values(2, “杰哥”, 30)
4 select * from user where num > 1

如上表, Session A 中的事务先根据条件 num > 0 这个条件查询表 user,得到了 name 列值为 ‘彬彬’ 的记录;之后 Session B 中提交了一个隐式事务,该事务向表 user 中插入了一条新记录;之后 Session A 中的事务再根据相同的条件 number > 0 查询表 user,得到的结果集中包含 Session B 中的事务新插入的那条记录,这种现象也被称之为 幻读。

小贴士:幻读其实是读到新插入的记录。假如发现再次读的时候记录变少了,这不是幻读,其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录

以上讲了这么多事务并发的问题,那我们如何解决这些问题呢?因此我们事务的隔离级别就出现了。

5、SQL标准中的四种隔离级别

SQL 中提供了四种隔离级别:

  • READ UNCOMMITTED :未提交读
  • READ COMMITTED :已提交读
  • REPEATABLE READ :可重复读
  • SERIALIZABLE :可串行化

针对这四种隔离级别能解决的问题我们在下面列一个表格:

隔离级别 脏写 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

✔:表示可能发生 ✖:表示不可能发生

  • READ UNCOMMITTED 隔离级别下,可能发生 脏读 、 不可重复读 和 幻读 问题。
  • READ COMMITTED 隔离级别下,可能发生 不可重复读 和 幻读 问题,但是不可以发生 脏读 问题。
  • REPEATABLE READ 隔离级别下,可能发生 幻读 问题,但是不可以发生 脏读 和 不可重复读 的问题。
  • SERIALIZABLE 隔离级别下,各种问题都不可以发生。

以上其实 SQL 标准中的隔离级别,现在让我们看看 MySQL 中的隔离级别,和 SQL 标准是有一些区别的哦。

6、MySQL中支持的四种隔离级别

MySQL 虽然支持4种隔离级别,但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL在REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的。

注意:这里不是说单独的 RR 级别就能解决幻读,在 RR 级别下可以配合 MVCC 或 GAP LOCK 或 NEXT-KEY 可以解决幻读。

MySQL 的默认隔离级别为 REPEATABLE READ,我们可以手动进行修改:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

也可以查询 MySQL 的隔离级别:

SELECT @@transaction_isolation;
# transaction_isolation是在MySQL 5.7.20的版本中引入来替换tx_isolation的

二、MVCC原理

好了,终于到了我们的重头戏了 😅😅😅,我不装了,我摊牌了,哈哈哈。

MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。 我们先看看啥是快照读与当前读:

1、快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是快照读 , 而非当前读 。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。

1 、快照读

快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞
读;比如这样:

SELECT * FROM player WHERE ...

之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

2 、当前读

当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁  

2、undo log 版本链

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列 :

  • trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id 赋值给 trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

废话不多说,我们还是以上面的 user 表为例(假设表里已经有了一条num=1的记录),直接上图:

image-20220327110258522

小贴士:select 查询操作不会生成 undo log!在 InnoDB 存储引擎中,undo log 只分为两种:

  • insert undo log:在 insert 操作中产生的 undo log
  • update undo log:对 delete 和 update 操作产生的 undo log

事实上,由于事务隔离性的要求,insert 操作的记录,只对事务本身可见,对其他事务不可见,也即插入操作不会对已经存在的记录产生影响!所以也就不存在并发情况下的问题。也就是说,MVCC 这个机制,其实就是靠 update undo log 实现的,和 insert undo log 基本上没啥关系,我们上面说的 undo log 版本链上的其实就是 update undo log,所以我们之后在画图时都会把 insert undo 给去掉。

假设之后两个事务 A 和 B的 事务 id 分别为 60 、 70 的事务对这条记录进行 UPDATE 操作,操作如下

发生时间编号 trx 60 trx 70
1 BEGIN
2 BEGIN
3 update user set name = ‘小兔老师’ where num = 1
4 update user set name = ‘彬彬’ where num = 1
5 COMMIT
6 update user set name = ‘杰哥’ where num = 1
7 update user set name = ‘阿伟’ where num = 1
8 COMMIT

每次对记录进行改动,都会记录一条 undo日志 ,每条 undo日志也都有一个 roll_pointer 属性( INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志 都连起来,串成一个链表,所以现在的情况就像下图一样:

image-20220327110304522

可以看到,每次修改行记录都会更新 trx_id 和 roll_pointer 这两个隐藏字段,之前的多个数据快照对应的 undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链,其中版本链的头节点就是当前记录最新的值。

3、ReadView

说完了 undo log 版本链,我们再说另外关于 MVCC 的核心概念,那就是 ReadView。

我们可以通过 ReadView 需要判断一下版本链中的哪个版本是当前事务可见的,以下是 ReadView 4个核心内容:

  • m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的事务id 列表。
  • min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务id ,也就是 m_ids 中的最小值,注意 max_trx_id 并不是 m_ids 中的最大值,事务id是递增分配的。
  • max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
  • creator_trx_id :表示生成该 ReadView 的事务的事务 id 。

那么这个 ReadView 是如何判断某个版本是否对当前事务可见呢?于是就有了如下规则:

  • 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_idmax_trx_id 之间,那就需要判断一下
    trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

只需要按照以上规则顺着 undo log 版本链找,如果没有找到则继续找下一个版本,直到最后一个版本。

小贴士:在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

接下来,再掏出 user 表,通过例子来理解下 ReaView 机制是如何做到判断当前事务能够看见哪些版本的。

1、READ COMMITTED

READ COMMITTED 隔离级别下每次读取数据前都生成一个ReadView,假如我们表里事先有如下数据:

image-20220327110315145

现在系统里有两个事务 id 分别为 60 、 70 的事务在执行:

# Transaction 60
BEGIN;
UPDATE user SET name = '小兔老师' WHERE num = 1;
UPDATE user SET name = '彬彬' WHERE num = 1;
# Transaction 70
BEGIN;
# 更新了一些别的表的记录
...

此刻,表 user 中 num 为 1 的记录得到的版本链表如下所示:

image-20220327110322425

假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 60、70未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为 '华强'

这是为啥呢,我们往下看 🤣🤣🤣

在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [60, 70] ,min_trx_id 为 60 , max_trx_id 为 71 , creator_trx_id 为 0 。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 ‘彬彬’ ,该版本的 trx_id 值为 60 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 ‘小兔老师’ ,该版本的 trx_id 值也为 60 ,也在 m_ids 列表内,所以也不符要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 ‘华强’ ,该版本的 trx_id 值为 50 ,小于 ReadView 中的 min_trx_id 值60 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘华强’ 的记录。

之后,我们把 事务id 为 60 的事务提交一下,就像这样:

# Transaction 60
BEGIN;
UPDATE user SET name = '小兔老师' WHERE num = 1;
UPDATE user SET name = '彬彬' WHERE num = 1;
COMMIT;

然后再到事务id 为 70 的事务中更新一下表 user 中 num 为 1 的记录:

# Transaction 70
BEGIN;
# 更新了一些别的表的记录
...
UPDATE user SET name = '杰哥' WHERE num = 1;
UPDATE user SET name = '阿伟' WHERE num = 1;

此刻,表 user 中 num 为 1 的记录的版本链就长这样:

image-20220327110331812

然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 num 为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 60、70均未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为'华强'
# SELECT2:Transaction 60提交,Transaction 70未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为'彬彬'

这个 SELECT2 的执行过程如下:

在执行 SELECT 语句时会又会单独生成一个 ReadView ,该 ReadView 的 m_ids 列表的内容就是 [70] ( 事务id 为 60 的那个事务已经提交了,所以再次生成快照时就没有它了), min_trx_id 为 70 ,max_trx_id 为 71 , creator_trx_id 为 0 。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 ‘阿伟’ ,该版本的trx_id 值为 70 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 ‘杰哥’ ,该版本的 trx_id 值为 70 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 ‘彬彬’ ,该版本的 trx_id 值为 60 ,小于 ReadView 中的 min_trx_id 值70 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘彬彬’ 的记录。

以此类推,如果之后 事务id 为 70 的记录也提交了,再此在使用 READ COMMITTED 隔离级别的事务中查询表 user 中 num 值为 1 的记录时,得到的结果就是 “阿伟” 了。

总结:使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。

2、REPEATABLE READ

REPEATABLE READ 隔离级别在第一次读取数据时生成一个ReadView。

我们还是以上面的例子为例:😂😂😂

比方说现在系统里有两个 事务id 分别为 60 、 70 的事务在执行:

# Transaction 60
BEGIN;
UPDATE user SET name = '小兔老师' WHERE num = 1;
UPDATE user SET name = '彬彬' WHERE num = 1;
# Transaction 70
BEGIN;
# 更新了一些别的表的记录
...

此刻,表 user中 num 为 1 的记录得到的版本链表如下所示 :

image-20220327110340991

假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 60、70未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为 '华强'

这是为啥呢,我们往下看 🤣🤣🤣

在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [60, 70] ,min_trx_id 为 60 , max_trx_id 为 71 , creator_trx_id 为 0 。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 ‘彬彬’ ,该版本的 trx_id 值为 60 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 ‘小兔老师’ ,该版本的 trx_id 值也为 60 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 ‘华强’ ,该版本的 trx_id 值为 50 ,小于 ReadView 中的 min_trx_id 值60 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘华强’ 的记录。

之后,我们把 事务id 为 60 的事务提交一下,就像这样:

# Transaction 60
BEGIN;
UPDATE user SET name = '小兔老师' WHERE num = 1;
UPDATE user SET name = '彬彬' WHERE num = 1;
COMMIT;

然后再到 事务id 为 70 的事务中更新一下表 user 中 num 为 1 的记录:

# Transaction 70
BEGIN;
# 更新了一些别的表的记录
...
UPDATE user SET name = '杰哥' WHERE num = 1;
UPDATE user SET name = '阿伟' WHERE num = 1;

此刻,表 user 中 num 为 1 的记录的版本链就长这样:

image-20220327110347419

然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 num 为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 60、70均未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为'华强'
# SELECT2:Transaction 60提交,Transaction 70未提交
SELECT * FROM user WHERE num = 1; # 得到的列name的值为'彬彬'

这个 SELECT2 的执行过程如下:

因为当前事务的隔离级别为 REPEATABLE READ ,而之前在执行 SELECT1 时已经生成过 ReadView 了,所以此时直接复用之前的 ReadView (区分 RC 隔离级别下是会重新生成新的 Read View的),之前的 ReadView 的 m_ids 列表的内容就是 [60, 70] , min_trx_id 为 60 , max_trx_id 为 71 ,creator_trx_id 为 0 。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 ‘阿伟’ ,该版本的trx_id 值为 70 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是 ‘杰哥’ ,该版本的 trx_id 值为 70 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是 “彬彬” ,该版本的 trx_id 值为 60 ,而 m_ids 列表中是包含值为 60 的事务 id 的,所以该版本也不符合要求,同理下一个列 name 的内容是 ‘小兔老师’ 的版本也不符合要求。继续跳到下一个版本。

下一个版本的列 name 的内容是 ‘华强’ ,该版本的 trx_id 值为 50 ,小于 ReadView 中的 min_trx_id 值60 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列为 ‘华强’ 的记录。

4、如何解决幻读

接下来说明InnoDB 是如何解决幻读的。
假设现在表 student 中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的 undo log 如下图所示。

image-20220327110355164

假设现在有事务 A 和事务 B 并发执行, 事务 A 的事务 id 为 20 , 事务 B 的事务 id 为 30 。
步骤1:事务 A 开始第一次查询数据,查询的 SQL 语句如下。

select * from student where id >= 1;

在开始查询之前,MySQL 会为事务 A 产生一个 ReadView,此时 ReadView 的内容如下: m_ids [20,30] , min_trx_id=20 , max_trx_id=31 , creator_trx_id=20 。
由于此时表 student 中只有一条数据,且符合 where id>=1 条件,因此会查询出来。然后根据 ReadView 机制,发现该行数据的trx_id=10,小于事务 A 的 ReadView 里 min_trx_id,这表示这条数据是事务 A 开启之前,其他事务就已经提交了的数据,因此事务 A 可以读取到。
结论:事务 A 的第一次查询,能读取到一条数据,id=1。
步骤2:接着事务 B(trx_id=30),往表 student 中新插入两条数据,并提交事务。

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');

此时表student 中就有三条数据了,对应的 undo 如下图所示:

image-20220327110401870

步骤3:接着事务 A 开启第二次查询,根据可重复读隔离级别的规则,此时事务 A 并不会再重新生成ReadView。此时表 student 中的 3 条数据都满足 where id>=1 的条件,因此会先查出来。然后根据ReadView 机制,判断每条数据是不是都可以被事务 A 看到。
1)首先 id=1 的这条数据,前面已经说过了,可以被事务 A 看到。
2)然后是 id=2 的数据,它的 trx_id=30,此时事务 A 发现,这个值处于 min_trx_id和 max_trx_id之间,因此还需要再判断 30 是否处于 m_ids 数组内。由于事务 A 的 m_ids =[20,30],因此在数组内,这表示 id=2 的这条数据是与事务 A 在同一时刻启动的其他事务提交的,所以这条数据不能让事务 A 看到。
3)同理,id=3 的这条数据,trx_id 也为 30,因此也不能被事务 A 看见。

image-20220327110407790

结论:最终事务 A 的第二次查询,只能查询出 id=1 的这条数据。这和事务 A 的第一次查询的结果是一样
的,因此没有出现幻读现象,所以说在 MySQL 的可重复读隔离级别下,不存在幻读问题。

5、MVCC小结

讲了这么多,终于讲完了,现在让我们来总结一下吧 🤪🤪🤪

所谓的 MVCC (Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD 、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。 READ COMMITTD 、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个ReadView 就好了。

巨人的肩膀:

https://juejin.cn/book/6844733769996304392


文章作者: Skyu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Skyu !
  目录