进学阁

业精于勤荒于嬉,行成于思毁于随

0%

事务处理

事务的四大特性

  1. 原子性(Atomicity) 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性(Consistency) 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 :::tips
    拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

:::

  1. 隔离性(Isolation) 并发访问数据库时,即一个事务内部的操作及使用的数据对并发的其他事务是隔离
    的,并发执行的各个事务之间不能互相干扰。
  2. 持久性(Durability) 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

事务产生的问题

脏读(Drity Read)

已知有两个事务A和B, A读取了已经被B更新但还没有被提交的数据。之后,B回滚事务,A读取的数据就是脏数据。即:一个事务在执行的过程中读取到了其他事务还没有提交的数据。

:::tips
此处的”脏”主要指由于回滚,导致了数据的无效性,也就是读到了脏数据。

:::

脏读,可以简单理解为读到了无效数据。

不可重复读(Non-repeatable read)

已知有两个事务A和B,A多次读取同一数据,B在A多次读取的过程中对数据作了修改并提交,导致A多次读取同一数据时,结果不一致

此处的”重复”指的是由于修改,某个事务重复读取的某个值发生了变化。

不可重复读,可简单理解为多次重复读取,读取到了不一样的数据。

幻读(Phantom Read)

 幻读在可重复读的模式下才会出现,其他隔离级别中不会出现。

  有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行。简单地说,一个事务中先后读取一个范围的记录,但每次读取的纪录数不同,多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读区别

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

事务的隔离级别

了解决多个事务之间数据可见性及数据正确性的问题,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted(读未提交)、Read committed(读已提交)、Repeatable read(可重复读)、Serializable(串行)。

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。
InnoDB存储引擎在分布式事务的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

查看隔离级别命令
1
2
-- 查看隔离级别命令
show variables like 'transaction_isolation';

要修改隔离级别的话,可以进行全局修改,也就是修改配置文件(my.ini),示例:

1
2
transaction-isolation=READ-UNCOMMITTED

也可以通过命令修改当前session的隔离级别:

1
set session transaction isolation level 事务级别;
各种隔离级别中会出现的问题
  • 读未提交 存在脏读,不可重复读,幻读的问题
  • 读已提交 处理了脏读的问题但是存在不可重复读和幻读的问题
  • 可重复读 处理了不可重复读的问题,幻读依然存在 ,但是mysql中的这一级别因为mvcc的存在已经将幻读的问题也处理了
  • 串型化 都一个一个处理了肯定不存在脏读 重复读 幻读的问题
READ-UNCOMMITTED(读未提交)

最低的隔离级别,允许一个事务可以读取另外一个事务未提交的事务。多次读取结果不一样,会出现了脏读、不可重复读问题。
这个级别的性能没有足够大的优势,但是又有很多的问题,因此很少使用。

READ-COMMITTED(读已提交)*

允许一个事务读取另一个并发事务已经提交的数据。读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读。

REPEATABLE-READ(可重复读)*

对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。即使数据被其他事务修改, 当前事务也不会读取到新的数据。重复读事务中的查询看到的是事务开始时的快照, 而不是该事务当前查询开始时的快照。

  可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。

SERIALIZABLE(串行)*

最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰(多个事务之间读写、写读、写写会产生互斥,效果就是串行执行,多个事务之间的读读不会产生互斥)。

:::tips
该级别可以防止脏读、不可重复读以及幻读。

:::

Mysql中的事务操作

 Mysql中事务默认是隐式事务,执行insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。

:::tips
  事务分为隐式事务和显式事务。是否开启隐式事务是由变量 autocommit 控制的。可以通过set session autoCommit = on/off来设置mysql事务是否自动开启。如果我们设置autoCommit为off的时候,需要手动开启mysql事务。

:::

隐式事务
事务自动开启、提交或回滚,比如insert、update、delete语句,事务的开启、提交或回滚由Mysql内部自动控制的。

实际项目开发时,用隐式事务即可。

显式事务

需要手动开启、提交或回滚,由开发者自己控制。有两种方式:

1
2
3
4
5
6
7
8
9
10
11
-- 方式1
//设置不自动提交事务
set autocommit=0;
//执行事务操作
commit|rollback;

-- 方式2
start transaction;//开启事务
//执行事务操作
commit|rollback;

savepoint

如果在事务中我们执行了一大批操作,可只想回滚部分数据。此时可以将一大批操作分为几个部分,然后指定回滚某个部分,通过savepoint(保存点)来实现。示例:

1
2
3
4
5
6
7
8
9
10
start transaction;
insert into test1 values (1);
-- 设置一个保存点
savepoint part1;
insert into test1 values (2);
-- 将savepint = part1的语句到当前语句之间所有的操作回滚
rollback to part1;
-- 提交事务
commit;

只读事务

表示在事务中执行的是一些只读操作,如查询,但是不会做insert、update、delete操作。开启只读事务后,执行修改操作会报错。

查看变量 autocommit 是否开启了自动提交命令:
1
2
show variables like 'autocommit';

ACID的保证

事务的原子性是通过 undo log 来实现的。
事务的持久性是通过 redo log 来实现的。
事务的隔离性是通过 (读写锁+MVCC)来实现的。
事务的一致性是通过原子性、持久性、隔离性来实现的。

原子性

1、每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上。

2、所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。

持久性

隔离性

MySQL定义了4种隔离级别。隔离性是要做到什么呢? 隔离性是要管理多个并发读写请求的访问顺序。MVCC和读写锁都能达到一定的隔离效果。

一致性

数据库总是从一个一致性的状态转移到另一个一致性的状态。通过回滚,以及恢复,和在并发环境下的隔离可以达到一致性的效果。

InnoDB的幻读问题

隔离级别REPEATABLE-READ(可重复读)的时候,并没有解决幻读的问题,串行的话性能无法保证,所以在InnoDB引擎中引入了mvcc和间隙锁以及行锁来处理

快照读

InnoDB引擎通过mvcc来解决快照读的幻读问题,生成一个ReadView然后结合undo日志来解决幻读的问题

当前读

InnoDB引擎通过间隙锁来解决幻读问题,事务A和事务B产生并发,事务A未提交时阻塞事务B,这样就不会产生幻读问题

MVCC基础概念

:::tips
数据库通过加锁,可以实现事务的隔离性,串行化隔离级别就是加锁实现的,但是加锁会降低数据库性能

因此,数据库引入了MVCC多版本并发控制,在读取数据不用加锁的情况下,实现读取数据的同时可以修改数据,修改数据时同时可以读取数据。

:::

什么是MVCC

MVCC(Mutil-Version Concurrency Control),多版本并发控制。是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。用于支持读已提交(RC)和可重复读(RR)隔离级别的实现。

:::tips
MVCCMySQL InnoDB引擎中的实现主要是为了在处理读-写冲突时提高数据库并发性能,记录读已提交和可重复读这两种隔离级别下事务操作版本连的过程。

:::

  • 数据库并发场景一般有三种:
    • 读-读:不存在任何问题,不需要并发控制
    • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能会有脏读,幻读,不可重复读
    • 写-写:有线程安全问题,可能会存在更新丢失问题。
  • MVCC主要是用来解决【读-写】冲突的无锁并发控制,可以解决以下问题:
    • 在并发读写数据时,可以做到在读操作时不用阻塞写操作,写操作不用阻塞读操作,提高数据库并发读写的性能
    • 可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决【写-写】引起的更新丢失问题
  • MVCC与锁的组合

一般数据库中都会采用以上MVCC与锁的两种组合来解决并发场景的问题,以此最大限度的提高数据库性能

- <font style="color:rgb(79, 79, 79);">MVCC + 悲观锁</font><font style="color:rgb(79, 79, 79);">MVCC解决读-写冲突,悲观锁解决写-写冲突。</font>
- <font style="color:rgb(79, 79, 79);">MVCC + 乐观锁MVCC解决读-写冲突,乐观锁解决写-写冲突。</font>

:::tips
通过上述描述,MVCC的作用可以概括为就是为了解决【读写冲突】,提高数据库性能的,而MVCC的实现又依赖于六个概念:【隐式字段】【undo日志】【版本链】【快照读和当前读】【读视图】。

:::

隐式字段

在InnoDB存储引擎,针对每行记录都有固定的两个隐藏列【DB_TRX_ID】【DB_ROLL_PTR】以及一个可能存在的隐藏列【DB_ROW_ID】

隐式字段 描述 是否必须存在
DB_TRX_ID 事物Id,也叫事物版本号,占用6byte的标识,事务开启之前,从数据库获得一个自增长的事务ID,用其判断事务的执行顺序
DB_ROLL_PTR 占用7byte,回滚指针,指向这条记录的上一个版本的undo log记录,存储于回滚段(rollback segment)中
DB_ROW_ID 隐含的自增ID(隐藏主键),如果表中没有主键和非NULL唯一键时,则会生成一个单调递增的行ID作为聚簇索引

表中的数据会因此分为两种形式:

  • 有主键或唯一非空字段

  • 没有主键且没有唯一非空字段


undo日志

一种用于撤销回退的日志,在事务开始之前,会先记录存放到 Undo 日志文件里,备份起来,当事务回滚时或者数据库崩溃时用于回滚事务。

undo日志的主要作用是事务回滚和实现MVCC快照读

undo log日志分为两种

  • insert undo log代表事务在insert新记录时产生的undo log, 仅用于事务回滚,并且在事务提交后可以被立即丢弃
  • update undo log事务在进行updatedelete时产生的undo log; 不仅在事务回滚时需要,在实现MVCC快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被清理线程统一清除。

MVCC实际上是使用的update undo log 实现的快照读。

:::tips
InnoDB 并不会真正地去开辟空间存储多个版本的行记录,只是借助 undo log 记录每次写操作的反向操作。所以B+ 索引树上对应的记录只会有一个最新版本,InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。

:::

版本链

:::tips
一致性非锁定读是通过 MVCC 来实现的。但是MVCC 没有一个统一的实现标准,所以各个存储引擎的实现机制不尽相同。InnoDB 存储引擎中 MVCC 的实现是通过 undo log 来完成的

:::

当事务对某一行数据进行改动时,会产生一条Undo日志,多个事务同时操作一条记录时,就会产生多个版本的Undo日志,这些日志通过回滚指针(DB_ROLL_PTR)连成一个链表,称为版本链

只要有事务写入数据时,就会产生一条对应的 undo log,一条 undo log 对应这行数据的一个版本,当这行数据有多个版本时,就会有多条 undo log 日志,undo log 之间通过回滚指针(DB_ROLL_PTR)连接,这样就形成了一个 undo log 版本链。

快照读和当前读
快照读:

也叫普通读,读取的是记录数据的可见版本,不加锁,不加锁的普通select语句都是快照读,即不加锁的非阻塞读

快照读的执行方式是生成 ReadView,直接利用 MVCC 机制来进行读取,并不会对记录进行加锁

当前读:

也称锁定读【Locking Read】,读取的是记录数据的最新版本,并且需要先获取对应记录的锁

1
2
3
4
5
SELECT * FROM student LOCK IN SHARE MODE;  # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
读视图

Read View提供了某一时刻事务系统的快照,主要是用来做可见性判断, 里面保存了【对本事务不可见的其他活跃事务】

当事务在开始执行的时候,会产生一个读视图(Read View),用来判断当前事务可见哪个版本的数据,即可见性判断

实际上在innodb中,每个SQL语句执行前都会生成一个Read View

读视图的四个属性

MySQL5.7源码中对Read View定义了四个属性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ReadView {
private:
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id;

/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
trx_id_t m_up_limit_id;

/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id;

/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;

/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
trx_id_t m_low_limit_no;

/** AC-NL-RO transaction view that has been "closed". */
bool m_closed;

typedef UT_LIST_NODE_T(ReadView) node_t;

/** List of read views in trx_sys */
byte pad1[64 - sizeof(node_t)];
node_t m_view_list;
};
  • creator_trx_id ****创建当前read view的事务ID
  • m_ids ****当前系统中所有的活跃事务的 id,活跃事务指的是当前系统中开启了事务,但还没有提交的事务;
  • m_low_limit_id ****表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
  • m_up_limit_id ****当前系统中事务的 id 值最大的那个事务 id 值再加 1,也就是系统中下一个要生成的事务 id。

ReadView 会根据这 4 个属性,结合 undo log 版本链,来实现 MVCC 机制,决定一个事务能读取到数据那个版本

:::tips
假设现在有事务 A 和事务 B 并发执行,事务 A 的事务 id 为 10,事务 B 的事务 id 为 20。

事务A的ReadView :m_ids=[10,20],m_low_limit_id=10,m_up_limit_id=21,creator_trx_id=10。

事务B的ReadView :m_ids=[10,20],m_low_limit_id=10,m_up_limit_id=21,creator_trx_id=20。

:::

读视图可见性判断规则

将Read View中的活跃事务Id按照大小放在坐标轴上表示的话,如下图:

当一个事务读取某条数据时,会通过DB_TRX_ID【Uodo日志的事务Id】在坐标轴上的位置来进行可见性规则判断,如下:

  • DB_TRX_ID < m_low_limit_id

表示DB_TRX_ID对应这条数据【Undo日志】是在当前事务开启之前,其他的事务就已经将该条数据修改了并提交了事务(事务的 id 值是递增的),所以当前事务【开启Read View的事务】能读取到。

  • DB_TRX_ID >= m_up_limit_id

表示在当前事务【creator_trx_id】开启以后,有新的事务开启,并且新的事务修改了这行数据的值并提交了事务,因为这是【creator_trx_id】后面的事务修改提交的数据,所以当前事务【creator_trx_id】是不能读取到的。

  • m_low_limit_id =< DB_TRX_ID < m_up_limit_id
    • DB_TRX_ID 在 m_ids 数组中

示DB_TRX_ID【写Undo日志的事务】 和当前事务【creator_trx_id】是在同一刻开启

DB_TRX_ID 不等于creator_trx_id

DB_TRX_ID事务修改了数据的值,并提交了事务,所以当前事务【creator_trx_id】不能读取到。

- **<font style="color:rgb(37, 132, 181);">DB_TRX_ID  等于creator_trx_id</font>**<font style="color:black;"></font>

表明数据【Undo日志】 是自己生成的,因此是可见

- **<font style="color:rgb(37, 132, 181);">DB_TRX_ID  不在 m_ids 数组中</font>**

表示的是在当前事务【creator_trx_id】开启之前,其他事务【DB_TRX_ID】将数据修改后就已经提交了事务,所以当前事务能读取到。

读视图可见性判断规则案例说明

了解了读视图可见性判断规则,下面通过一个场景案例图解的方式来详细逐条验证上述规则。一般来说,我们的行数据结构都为一下模式:

假设有一个事物【DB_TRX_ID = 10】在表中插入了一条数据,则它的数据结构为为:

  • 【第一步】:假设现在有事务 A【DB_TRX_ID = 20】 和事务 B 【DB_TRX_ID = 30】并发执行
1
2
3
4
#事物A:
select name from user where id = 1;
#事物B:
update user set name = 'edwin' where id = 1;

事物开始后分别生成ReadView

- **<font style="color:black;">事务A的ReadView :m_ids=[20,30],m_low_limit_id=20,m_up_limit_id=31,creator_trx_id=20。</font>**
- **<font style="color:black;">事务B的ReadView :m_ids=[20,30],m_low_limit_id=20,m_up_limit_id=31,creator_trx_id=30。</font>**
  • 【第二步】:事物A开启事物之后通过版本链第一次读取数据,版本链中的DB_TRX_ID = 10,小于事物A的【DB_TRX_ID = 20】,说明DB_TRX_ID = 10这条数据是事物A开启之前就已经写入,并提交了事物,所以事物A可以读取到。

  • 【第三步】:事务 B 【DB_TRX_ID = 30】修改数据,将name修改为Edwin,修改后写入Undo Log日志,此时还没有提交事务B。示意图如下:

  • 【第四步】:事务A【DB_TRX_ID = 20】第二次去读取数据

在 undo log版本链中,数据最新版本的事务id为30,这个值处于事务A的 ReadView 里 m_low_limit_id 和 m_up_limit_id 并且存在于m_ids 数组中,表示这个版本的数据是和自己同一时刻启动的事务修改的,因此这个版本的数据,数据 A 读取不到****

此时需要沿着 undo log 的版本链向前找,接着会找到该行数据的上一个版本db_trx_id=10,由于db_trx_id=10小于 m_low_limit_id的值,因此事务 A 能读取到该版本的值,即事务 A 读取到的值是星之码。

  • 【第五步】:现在事务 B 提交,此时系统中活跃的事务只有事物A,事物A第三次读取,读取到内容就有两种可能性:

这里留一个问题一:造成这两种情况的原因是什么?

我们留到本文第三节【不同隔离级别MVCC实现原理】中说明,继续案例

- **<font style="color:rgb(37, 132, 181);">读已提交(RC)隔离级别:读取到是事物B提交的Edwin</font>****<font style="color:rgb(1, 1, 1);">。</font>**
- **<font style="color:rgb(37, 132, 181);">可重复读(RR)隔离级别:读取到是原始数据提交的星河之码</font>****<font style="color:rgb(1, 1, 1);">。</font>**
  • 【第六步】:新的事物C【DB_TRX_ID = 40】修改数据,将name修改为彬
1
2
#事物C:
update user set name = '彬' where id = 1;

执行脚本前生成的ReadView如下,执行脚本后,提交事物C。

事务C的ReadView :m_ids=[20,40],m_low_limit_id=20,m_up_limit_id=41,creator_trx_id=40

  • 【第七步】:事务 A【DB_TRX_ID = 20】第四次读取数据,

此时由于事物A,由于事物A的m_up_limit_id=31,而日志中的DB_TRX_ID=40,根据可见性判断规则可以知到,事物A不能读取到DB_TRX_ID=40的记录,按照版本链的DB_POLL_PTR继续往上找,找到DB_TRX_ID=30的记录,虽然30在事物A的的m_ids=[20,30],但是DB_TRX_ID=30不等于事物A的creator_trx_id=20,所以还是不能读取,继续往上找,最终读取到了DB_TRX_ID=10的记录,name=星河之码

:::tips
实际上,这里事务A在不同场景下也是可以读取到DB_TRX_ID=40得数据的。

这里也留一个问题二:在什么场景下能够读取到DB_TRX_ID=40得数据name=彬呢?

我们留到本文第三节【不同隔离级别MVCC实现原理】中说明,继续案例

:::

  • 【第八步】:事务 A【DB_TRX_ID = 20】开始修改数据,将name 修改为 ‘法外狂徒张三’
1
2
#事物A:
update user set name = '法外狂徒张三' where id = 1;

此时事务A还没有提交,但是已经写入了Undo 日志,新的版本链如下

  • 【第九步】:事务 A第五次读取数据

由于Undo日志中的最新数据DB_TRX_ID=20等于事物A的creator_trx_id=20,说明是自己修改的数据,可以查到,name=法外狂徒张三

通过以上九个步骤图解的方式,对读视图可见性判断规则做了分析,通过ReadView 和 undo log分析了MVCC 的实现原理,接下来结合事务的隔离级别,看看MVCC是怎么读取数据的。

不同隔离级别MVCC实现原理
MVCC实现原理

通过上述对【Read View】的分析可以总结出:InnoDB 实现MVCC是通过 Read View与Undo Log 实现的,Undo Log 保存了历史快照,形成版版本链,Read View可见性规则判断当前版本的数据是否可见

InnnoDB执行查询语句的具体步骤为

  • 执行语句之前获取查询事务自己的事务Id,即事务版本号。
  • 通过事务id获取Read View
  • 查询存储的数据,将其事务Id与Read View中的事务版本号进行比较
  • 不符合Read View的可见性规则,则读取Undo log中历史快照数据
  • 找到当前事务能够读取的数据返回

而在实际的使用过程中,Read View在不同的隔离级别下是得工作方式是不一样

读已提交(RC)MVCC实现原理

在读已提交(Read committed)的隔离级别下实现MVCC,同一个事务里面,【每一次查询都会产生一个新的Read View副本】,这样可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)

还是按照上述案例来说明一下:

  • 【第一步】:准备一条原始数据

  • 【第二步】:假设现在有事务 A【DB_TRX_ID = 20】 和事务 B 【DB_TRX_ID = 30】并发执行
1
2
3
4
#事物A:
select name from user where id = 1;
#事物B:
update user set name = 'edwin' where id = 1;

执行过程为

时间 事务A 事务B
1 开始事务
2 第一次查询:select name from user where id = 1;
3 开始事务
4 执行修改:update user set name = ‘edwin’ where id = 1;
5 ****提交事务
6 第二次查询:select name from user where id = 1;
7 提交事务
  • 版本链为:

案例结果分析

上述案例在在读已提交(Read committed)的隔离级别下实现,同一个事务里面,【每一次查询都会产生一个新的Read View副本】。所以第二步实际上产生了三个Read View

m_ids m_low_limit_id m_up_limit_id creator_trx_id
事务A:第一次查询Read View [20,30] 20 31 20
事务B:Read View [20,30] 20 31 30
事务A:第二次查询Read View [20] 20 31 20

通过可见性判断:

  • 事务A第一次查询时

日志事务Id【DB_TRX_ID = 10】 < 最小活跃事务ID【m_low_limit_id=20】,因此可以读取到DB_TRX_ID = 10这条版本链中的数据。即name = 星河之码。

  • 事务A第二次查询时

此时事务B已经提交,版本链中最新版本为DB_TRX_ID = 30,而可见性规则中虽然满足

【m_low_limit_id=20】=<【DB_TRX_ID=30】<【m_up_limit_id=31】但是【DB_TRX_ID=30】不在m_ids集合[20]中,因此事务A的第二次查询可以读取【DB_TRX_ID=30】的数据,即name = edwin。

案例总结

通过上述案例说明,同一个事务A的两个相同查询,第一次结果为星河之码,第二次结果为edwin,因此在读已提交(RC)隔离级别下,存在不可重复读并发问题

可重复读(RR)MVCC实现原理

在可重复读(Repeatable read)的隔离级别下实现MVCC,【同一个事务里面,多次查询,都只会产生一个共用Read View】,以此不可重复读并发问题

案例与读已提交一样,这里就不重复赘述,可以再看一遍读已提交的【第一步】【第二步】,直接进行案例分析

案例结果分析

由于同一个事物只会产生一个共用Read View,所以可重复读的隔离级别下第二步只产生了两个Read View

上述案例在可重复读(Repeatable read),【每一次查询都会产生一个新的Read View副本】。所以第二步实际上产生了三个Read View

m_ids m_low_limit_id m_up_limit_id creator_trx_id
事务A:Read View [20,30] 20 31 20
事务B:Read View [20,30] 20 31 30

通过可见性判断:

  • 事务A第一次查询时

日志事务Id【DB_TRX_ID = 10】 < 最小活跃事务ID【m_low_limit_id=20】,因此可以读取到DB_TRX_ID = 10这条版本链中的数据。即name = 星河之码。

  • 事务A第二次查询时

此时事务B已经提交,版本链中最新版本为DB_TRX_ID = 30,而可见性规则中虽然满足

【m_low_limit_id=20】=<【DB_TRX_ID=30】<【m_up_limit_id=20】并且【DB_TRX_ID=30】也在m_ids集合[20,30]中,但是【DB_TRX_ID=30】不等于事物A的【creator_trx_id=20】,说明DB_TRX_ID=30是同一时刻其他事物提交的,事物A不能读取到,因此事物A只能按照版本链继续往上找,最终读取到【DB_TRX_ID=10】的数据,即name = 星河之码。

案例总结

通过上述案例说明,同一个事务A的两个相同查询,结果都为星河之码,因此在可重复读(RR)隔离级别下,解决了不可重复读并发问题

:::tips
其实读已经提交与可重复读的可见性判断的区别就在于事务A第二次查询时使用的Read View不同。

:::