type
status
date
slug
summary
tags
category
icon
password
创建时间
Nov 27, 2024 12:06 PM
我们在上次说到了数据库的“原子性”和“持久性”。这次我们来说说隔离性。我们把最后隔离性弄清楚,一致性就得到了保证了(一致性是我们的目标,持久性、原子性、隔离性满足后,一致性就满足)。
事务隔离级别对应问题
我们都知道事务有四个隔离级别
- 读未提交:一个事务还没有提交,但是另一个事务,就可以在执行中看到此事务对数据的操作;
- 读已提交:一个事务提交以后,另一个事务才能在执行中看到此事务对数据的操作。
- 可重复读:一个事务不管有没有提交事务(在另一个事务执行中提交),另一个事务在执行中,都不会看到此事务对数据的操作。
- 串行化:事务是一个接着一个执行的,一个事务没有执行完成,另一个事务不会开始启动。
四种隔离级别,分别可能事务并发中出现的问题:
- 读未提交:脏读、不可重复读、幻读
- 读已提交:
脏读、不可重复读、幻读
- 可重复读:
脏读、不可重复读、幻读
- 串行化:
脏读、不可重复读、幻读
为了更好地理解不同隔离级别下数据的读取情况,我们举个例子。

四种隔离级别,看看 V1,V2,V3 值的不同:
- 读未提交:
- V1:100
- V2:100
- V3:100
- 读已提交:
- V1:90
- V2:100
- V3:100
- 可重复读:
- V1:90
- V2:90
- V3:100
- 串行化:
- V1:90
- V2:90
- V3:100
注意:虽然可重复读和串行化在此条件下,得到的最后值是相同的。但是气背后的逻辑是不一样。
在可重复读中,V1,V2 的值为 90,是因为事务 B 在执行的过程中,对事务 A 是不可见的,因此不会影响事务 A;
而在串行化中,因为事务 A 先启动,事务 B 因为存储读写冲突,压根不会启动,自然不会影响事务 A 了。
有了上面的例子,我们再来理解三个问题:脏读、不可重复读、幻读
- 脏读:一个事务读取了另一个未提交事务修改的数据。还是上面的例子,如果在在查询分数 V1 后,事务 B 没有提交,进行了回滚。此时事务 A 操作的数据 V1 不就是无效的嘛
- 不可重复读:在同一事务中,两次读取同一行数据却得到了不同的结果。有了上面的问题,我们进化出了“读已提交”。此时,确实 V1 的值不会受到影响了,但是 V2 的值好像不对啊。怎么在一次事务中,两次相同的查询的分数不一致啊?
- 幻读:在同一事务中,两次执行相同查询,却返回了不同数量的记录。我们“又双”进化了“可重复读”,此时在一次事务中,V1 和 V2 的值终于相等了。但是,我们在随后的业务中发现其他问题,存在偶然的查询的行数不太多,或者在一个事务中,前一秒没查到数据,后一秒就查询到了。——幻读不好理解,我单独开一个标题。
注意:幻读仅专指“新插入的行”!!!(因修改数据和当前读引发的记录数据不一致不属于幻读)。
最后,我们干脆直接一个一个弄吧,省的相互干扰。
Read View
什么是 Read View?
InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
它们的区别在于创建 Read View 的时机不同:
- 读提交隔离:在每次读取数据时都会重新生成一个 Read View。(注意:不是每一条语句)
- 可重复读隔离:在启动事务时生成一个 Read View,整个事务期间都使用这个 Read View。
Read View 的基本构成
Read View 的四个字段:
m_ids
:指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表。注意是一个列表,“活跃事务”指的是启动了但还没提交的事务。
min_trx_id
:指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
max_trx_id
:这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,是全局事务中最大的事务 id 值 + 1;
creator_trx_id
:指的是创建该 Read View 的事务的事务 id。
Read View 的基本原理
Read View 通常与 MVCC(Multi Version Concurrency Controller,多版本并发控制)一起,而在 MVCC 中我还需要引入数据行的隐藏字段(有三个,在事务中用的是两个):
trx_id
:事务id,表示这个数据是由哪个事务生成的。- 当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在
trx_id
trx_id
占用 6 个字节
roll_pointer
:这条记录上一个版本的指针- 每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中。而这个指针会指向每一个旧版本记录
roll_pointer
占用 7 个字节。


一个事务去访问记录的时候,有以下情况:
-
trx_id < min_trx_id
:这个版本的记录是在创建 Read View 前已经提交的事务生成的,该版本的记录对当前事务可见。
trx_id >= max_trx_id
:这个版本的记录是在创建 Read View 后才启动的事务生成的,该版本的记录对当前事务不可见。
min_trx_id <= trx_id < max_trx_id
:需要判断trx_id
是否在m_ids
列表中:trx_id
在m_ids
列表中:生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。trx_id
不在m_ids
列表中:生成该版本记录的活跃事务已经被提交,该版本的记录对当前事务可见。
四种隔离级别是如何工作的?
读未提交直接提交数据,串行化通过加锁来实现,不再讨论范围。(之后丰富吧,埋个坑)
我们还是以这个作为例子:

读已提交
读提交隔离:在每次读取数据时都会重新生成一个 Read View。
假设事务 A (事务 id 为 51)启动后,紧接着事务 B (事务 id 为52)也启动了。
- 事务 A 读取数据(生成 Read View),查询分数为 90;
m_ids
:[51]min_trx_id
:51max_trx_id
:52creator_trx_id
:51
此时的 Read View
此时的数据行字段:

- 事务 B 查询分数,并修改数据为 100;
m_ids
:[51, 52]min_trx_id
:51max_trx_id
:53creator_trx_id
:52
此时的 Read View
此时的数据行字段:

- 事务 A 读取数据(生成 Read View),查询分数为 V1(90);
m_ids
:[51, 52]min_trx_id
:51max_trx_id
:53——(创建 Read View 时当前数据库中应该给下一个事务的 id 值)creator_trx_id
:51
此时的 Read View
此时的数据行字段:

我们发现
min_trx_id < trx_id < max_trx_id
,trx_id
在 m_ids
列表中,因此生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。我们得到数据为 90- 事务 B 提交事务;
- 事务 A 读取数据(生成 Read View),查询分数为 V2(100);
m_ids
:[51]min_trx_id
:51max_trx_id
:53creator_trx_id
:51
此时的 Read View
此时的数据行字段:

我们发现
min_trx_id < trx_id < max_trx_id
,trx_id
不在在 m_ids
列表中,因此生成该版本记录的活跃事务已经被提交,该版本的记录对当前事务可见。我们得到数据为 100可重复读
可重复读隔离:在启动事务时生成一个 Read View,整个事务期间都使用这个 Read View。
假设事务 A (事务 id 为 51)启动后,紧接着事务 B (事务 id 为52)也启动了。
事务 A 的 Read View:
此时的 Read View
m_ids
:[51]
min_trx_id
:51
max_trx_id
:52
creator_trx_id
:51
事务 B 的 Read View:
此时的 Read View
m_ids
:[51, 52]
min_trx_id
:51
max_trx_id
:53
creator_trx_id
:52
- 事务 A 读取数据,查询分数为 90;

- 事务 B 查询分数,并修改数据为 100;
此时的数据行字段:

- 事务 A 读取数据,查询分数为 V1(90):
我们发现
trx_id >= max_trx_id
:这个版本的记录是在创建 Read View 后才启动的事务生成的,该版本的记录对当前事务不可见。找下一个版本,发现 score 是 90
- 事务 B 提交事务;
- 事务 A 读取数据,查询分数为 V2(90);
此时的数据行字段:

我们发现
trx_id >= max_trx_id
:这个版本的记录是在创建 Read View 后才启动的事务生成的,该版本的记录对当前事务不可见。找下一个版本,发现 score 是 90假设事务 B (事务 id 为 51)启动后,紧接着事务 A (事务 id 为52)也启动了。
事务 A 的 Read View:
此时的 Read View
m_ids
:[51, 52]
min_trx_id
:51
max_trx_id
:53
creator_trx_id
:52
事务 B 的 Read View:
此时的 Read View
m_ids
:[51]
min_trx_id
:51
max_trx_id
:52
creator_trx_id
:51
只看上面的第 5 步:
此时的数据行字段:

对于事务 A,我们发现
min_trx_id <= trx_id < max_trx_id
,并且 trx_id
在 m_ids
列表中,生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。因此,会沿着版本链,找到下一个,发现 trx_id < min_trx_id
,读到 score 数据为 90。在可重复中,即使事务 B 提交了,但是在事务 A 中根据比较,得出事务 B 还是没有提及,这一条数据我不能读。
幻读问题
未完待续…