Molet

幻读:听说我是被MVCC干掉的?

Molet 运维技术 2022-11-22 434浏览 0

我是幻读,听说有人认为我是 MVCC 解决的,为了让大家更全面地理解我,只能亲自来解释一下。

幻读:听说我是被MVCC干掉的?

图片来自 Pexels

我是谁?

先给大家做一个简单的自我介绍,我就是事务并发时会产生的三大问题之一。

我的其他俩兄弟脏读、不可重复读被 MVCC 在上一个回合无情的干掉了,至于上个回合发生了什么可以去看剧情回顾。

我的由来就是因为主人在操作一组数据时还有很多人也在对这组数据进行操作。

举一个简单的案例:根据条件在对一组数据进行过滤返回的结果为 100 个,但是在主人操作的同时其他人又新增了符合条件的数据,然后主人再次进行查询时返回结果为 101。第二次返回的数据跟第一次返回数据不一致。

于是我诞生了,大家还给我起了个很好听的名字幻读。为什么会给我起这个名字呢!那是因为我给人们的现象好像出了幻觉一样。

为什么有人会认为我是被 MVCC 干掉的

为了演示方便,就直接使用之前的测试表来进行操作。

幻读:听说我是被MVCC干掉的?

同时大家可以看到此表还有一些测试数据,一切从头开始,清空表。

清空表的命令:

truncatetable_name

执行这个命令会使表的数据清空,并且自增 ID 会从 1 开始。

从执行过程来看,truncate table 类似于 drop table 然后在 create table,这里的环境都是测试环境,千万不要在线上进行操作,因为它绕过了 DML 方法,是不能回滚的。

幻读:听说我是被MVCC干掉的?

进行了一点小插曲,进入正题。

幻读:听说我是被MVCC干掉的?

根据上图的执行步骤,预期来说左边事务的第一条 select 语句查询结果为空。第二个 select 查询结果为 1 条数据,包含右边事务提交的数据。

但在实际测试的情况下,第一次执行 select 和第二次执行 select 返回结果一致。

从这个案例中,可以得出结论确实在不可重复隔离级别下会解决幻读问题(在快照读的前提下)。

我真的是被 MVCC 解决的?

通过上述测试案例来看,貌似在 MySQL 中通过 MVCC 就解决我的引来的问题,那既然都解决了我的问题,为什么还有串行化的隔离级别呢!好疑惑啊!

带着这个疑问继续进行实验,为了方便就不再使用上边表结构了,建立一个简单的表结构。

幻读:听说我是被MVCC干掉的?

再进入一个小插曲你知道在 MySQL 终端如何清屏吗?执行命令 system clear 即可。

幻读:听说我是被MVCC干掉的?

接着开始新一轮的测试:

幻读:听说我是被MVCC干掉的?

上图案例事务 1 几次查询数据都是空。此时事务 2 已经成功将数据插入并且提交。但当事务 1 几次查询数据为空之后进行数据插入时,提示主键重复。

再来看一个案例:

幻读:听说我是被MVCC干掉的?

如上图:

  • step1:事务 1 开启事务
  • step2:事务 2 开启事务
  • step3:事务 1 查询数据只有一条数据
  • step4:事务 2 添加一条数据
  • step5:事务 1 查询数据为一条
  • step6:事务 2 提交事务
  • step7:事务 1 查询数据为一条
  • step8:事务 1 修改 name
  • step9:猜想一下此时表内数据会发生什么改变

幻读:听说我是被MVCC干掉的?

此案例中事务 1 始终读取数据都是一条数据,但是在修改数据时影响数据行数却是 2,再次进行查看数据时竟然出现了事务 2 添加的数据。这也可以看作是一种幻读。

小结:通过以上俩个案例得知在 MySQL 可重复读隔离级别中并没有完全解决幻读问题,而只是解决了快照读下的幻读问题。

而对于当前读的操作依然存在幻读问题,也就是说 MVCC 对于幻读的解决是不彻底的。

再聊当前读、快照读

在上一回合中快照读、当前读已经被消化了,为了防止消化不良这里再简单说明一下。

①当前读

所有操作都加了锁,并且锁之间除了共享锁都是互斥的,如果想要增、删、改、查时都需要等待锁释放才可以,所以读取的数据都是最新的记录。

简单来说,当前读就是加了锁的,增、删、改、查,不管锁是共享锁、排它锁均为当前读。

在 MySQL 的 Innodb 存储引擎下,增、删、改操作都会默认加上锁,所以增、删、改操作默认就为当前读。

②快照读

快照读的出现旨在提高事务并发性,实现基于我的敌人 MVCC,简单来说快照读就是不加锁的非阻塞读,即简单的 select 操作(select * from user)。

在 Innodb 存储引擎下执行简单的 select 操作时,会记录下当前的快照读数据,之后的 select 会沿用第一次快照读的数据,即使有其它事务提交也不会影响当前的 select 结果,这就解决了不可重复读问题。

快照读读取的数据虽然是一致的,但有可能不是最新的数据而是历史数据。

告诉你们吧!当前读的情况下,我是被 next-key locks 干掉的

第二小节中得知在快照读下由于我引发的问题已经被 MVCC 消灭了。但是在小节三进行案例测试发现在当前读下我又满血复活了。

我要是那么容易被干掉还怎么被称为打不死的小强,这不是闹笑话呢!说归说,闹归闹如果 MVCC 把它的小弟 next-key locks 带上那我就完了,就不再像灰太狼说经典语录“我一定会回来的”。

此时就要思考一个问题,在 Innodb 存储引擎下,是默认给快照读加 next-key locks,还是说需要手动加锁。

通过官方文档对于 next-key locks 的解释:

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.

大致意思,为了防止幻读,Innodb 使用 next-key lock 算法,将行锁(record lock)和间隙锁(gap lock)结合在一起。

Innodb 行锁在搜索或者扫描表索引时,会在遇到的索引记录上设置共享锁或者排它锁,因此行锁实际是索引记录锁。

另外, 在索引记录上设置的锁同样会影响索引记录之前的“间隙(gap)”。即 next-key lock 是索引记录行加上索引记录之前的“gap”上的间隙锁定。

并且还给了一个案例:

SELECT*FROMchildWHEREid>100FORUPDATE;

当 Innodb 扫描索引时,会将 id 大于 100 地上锁,阻止任何大于 100 的数据添加。

到这里就回答了上边问题,在 Innodb 下解决当前读产生的幻读问题需要手动加锁来解决。

再来看一个案例,下图为此时的数据情况:

幻读:听说我是被MVCC干掉的?

下图的这个案例就解决了在第三节中第一个案例的幻读问题。

幻读:听说我是被MVCC干掉的?

如上图:

  • step事务1:开启事务
  • step事务2:开启事务
  • step事务1:查询 ID 为 4 的这条数据并且加上排它锁
  • step事务2:添加 ID 为 4 的数据,并且等待事务 1 释放锁
  • step事务1:添加 ID 为 4 的数据,添加成功
  • step事务1:查询当前数据
  • step事务1:提交事务
  • step事务2:报错,返回主键重复问题

这个案例查询的索引列是主键并且是唯一的,此时 Innodb 引擎会对 next-key lock 做降级处理,也就是只锁定当前查询的索引记录行,而不是范围锁定。

案例二:还是使用上边的数据,但是这次我们进行一次范围查找。

幻读:听说我是被MVCC干掉的?

此时的数据为 1,3,5,查找的范围为大于 3。从下图可以看出当事务 2 执行添加 ID 为 2 的是可以添加成功的。

但是当添加 ID 6 时需要等待。此时若事务 1 不提交事务,事务 2 添加 ID 为 6 的这条数据就执行不成功。

幻读:听说我是被MVCC干掉的?

对于上述的 SQL 语句 select * from user where id > 3 for update;执行返回的只有 5 这一行数据。

此时锁定的范围为 (3,5],(5,∞),所以说 id 为 2 的可以插入,ID 为 4 或者大于 5 的都是插入不了的。

继续浏览有关 开发工具 的文章
发表评论