🗒️思考:MySQL 事务的隔离性
2024-11-27
| 2024-11-28
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
创建时间
Nov 27, 2024 12:06 PM
我们在上次说到了数据库的“原子性”和“持久性”。这次我们来说说隔离性。我们把最后隔离性弄清楚,一致性就得到了保证了(一致性是我们的目标,持久性、原子性、隔离性满足后,一致性就满足)。

事务隔离级别对应问题

我们都知道事务有四个隔离级别
  • 读未提交:一个事务还没有提交,但是另一个事务,就可以在执行中看到此事务对数据的操作;
  • 读已提交:一个事务提交以后,另一个事务才能在执行中看到此事务对数据的操作。
  • 可重复读:一个事务不管有没有提交事务(在另一个事务执行中提交),另一个事务在执行中,都不会看到此事务对数据的操作。
  • 串行化:事务是一个接着一个执行的,一个事务没有执行完成,另一个事务不会开始启动。
四种隔离级别,分别可能事务并发中出现的问题:
  • 读未提交:脏读、不可重复读、幻读
  • 读已提交:脏读、不可重复读、幻读
  • 可重复读:脏读不可重复读、幻读
  • 串行化:脏读不可重复读幻读
为了更好地理解不同隔离级别下数据的读取情况,我们举个例子。
notion image
四种隔离级别,看看 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 个字节。
notion image
notion image
一个事务去访问记录的时候,有以下情况:
  • 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_idm_ids 列表中:生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。
    • trx_id 不在 m_ids 列表中:生成该版本记录的活跃事务已经被提交,该版本的记录对当前事务可见。

四种隔离级别是如何工作的?

读未提交直接提交数据,串行化通过加锁来实现,不再讨论范围。(之后丰富吧,埋个坑)
我们还是以这个作为例子:
notion image

读已提交

读提交隔离:在每次读取数据时都会重新生成一个 Read View。
假设事务 A (事务 id 为 51)启动后,紧接着事务 B (事务 id 为52)也启动了。
  1. 事务 A 读取数据(生成 Read View),查询分数为 90;
    1. 此时的 Read View
      • m_ids:[51]
      • min_trx_id:51
      • max_trx_id:52
      • creator_trx_id:51
      此时的数据行字段:
      notion image
  1. 事务 B 查询分数,并修改数据为 100;
    1. 此时的 Read View
      • m_ids:[51, 52]
      • min_trx_id:51
      • max_trx_id:53
      • creator_trx_id:52
      此时的数据行字段:
      notion image
  1. 事务 A 读取数据(生成 Read View),查询分数为 V1(90);
    1. 此时的 Read View
      • m_ids:[51, 52]
      • min_trx_id:51
      • max_trx_id53——(创建 Read View 时当前数据库中应该给下一个事务的 id 值
      • creator_trx_id:51
      此时的数据行字段:
      notion image
      我们发现 min_trx_id < trx_id < max_trx_idtrx_idm_ids 列表中,因此生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。我们得到数据为 90
  1. 事务 B 提交事务;
  1. 事务 A 读取数据(生成 Read View),查询分数为 V2(100);
    1. 此时的 Read View
      • m_ids:[51]
      • min_trx_id:51
      • max_trx_id:53
      • creator_trx_id:51
      此时的数据行字段:
      notion image
      我们发现 min_trx_id < trx_id < max_trx_idtrx_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
  1. 事务 A 读取数据,查询分数为 90;
    1. notion image
  1. 事务 B 查询分数,并修改数据为 100;
    1. 此时的数据行字段:
      notion image
  1. 事务 A 读取数据,查询分数为 V1(90):
    1. 我们发现trx_id >= max_trx_id :这个版本的记录是在创建 Read View 后才启动的事务生成的,该版本的记录对当前事务不可见。找下一个版本,发现 score 是 90
      notion image
  1. 事务 B 提交事务;
  1. 事务 A 读取数据,查询分数为 V2(90);
    1. 此时的数据行字段:
      notion image
      我们发现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 步:
此时的数据行字段:
notion image
对于事务 A,我们发现 min_trx_id <= trx_id < max_trx_id ,并且 trx_idm_ids 列表中,生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本的记录对当前事务不可见。因此,会沿着版本链,找到下一个,发现 trx_id < min_trx_id,读到 score 数据为 90。
💡
在可重复中,即使事务 B 提交了,但是在事务 A 中根据比较,得出事务 B 还是没有提及,这一条数据我不能读。

幻读问题

未完待续…

📎 参考

  • Important
  • 2779. 数组的最大美丽值2958. 最多 K 个重复元素的最长子数组
    Loading...