「MySQL高级篇」MySQL幻读有被完全解决吗?

979 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第十二天,点击查看活动详情

大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生,最近在学MySQL高级篇,现在正在看面经整理遗漏知识点,欢迎各路大佬一起交流讨论

👉本篇速览

在前几篇文章中,我们详细讲解了MySQL事务的隔离性的实现,也就是如何去防止幻读,从MVCC的方式解决快照读当前读的幻读问题,可事实上这两个解决方案在很大程度上解决了幻读现象,但还是会有个别情况的幻读问题无法解决,那么在今天的文章中,我们将以以下几点讲述一下MySQL幻读相关的知识:

  • 1️⃣ 首先回顾一下快照读是如何避免幻读的
  • 2️⃣ 然后回顾一下当前读是如何避免幻读的
  • 3️⃣ MySQL中幻读发生的场景与现象

1️⃣ MVCC

在介绍MySQL幻读发生之前,我们首先简单的回顾一下MySQL是如何解决幻读的(ps:此处只是做一个简单的回顾,如果想要详细了解可以移步这篇文章:「MySQL高级篇」详解MVCC)

针对于快照读,MySQL是通过MVCC解决了幻读,这个我们在讲解MVCC的时候就提到,MVCC是用于解决快照读的幻读问题的,而MVCC的具体实现又依赖于数据库记录的三个隐式字段undo log日志readView

每次事务开始后,进行快照读,也就是第一个查询语句的时候,会生成一个ReadView,ReadView中包含的字段有:当前活跃的事务ID、最小的事务ID、最大事务ID+1、创建者的事务ID,而每次操作后数据都会记录在undo log中,会形成一个undo log版本链,记录的数据中含有的隐藏字段事务ID、回滚指针又是我们判断的主要依据。说了这么多,我们简单举个栗子:

我们只分析事务5的两个快照读:

首先第一个快照读,我们简单分析一下它生成的ReadView,此时尚未提交的事务为3,4,5,因此活跃事务ID集合[3,4,5],其中最小的为3,最大的加一为6,因此最小事务ID = 3,最大事务ID+1 = 6,创建这个快照读的事务为5,因此创建事务ID = 5

根据它的操作我们可以得到它的undo log版本链如下(ps:如果不知道该版本链如何生成,也可以去看这篇文章「MySQL高级篇」详解MVCC ):

现在我们就要借用这张图和四个规则开始匹配,根据已知的数据,我们可以把规则中的数据填上方便我们进行匹配:

从版本链的最新的数据开始遍历:

  1. 首先事务ID为4,发现四条规则都匹配不上:4 != 5, 4 > 3, 4 < 6, 4虽然在3-6之间,但是存在于m_ids中
  2. 然后拿到版本链的下一个数据,事务ID为3:3 != 5, 3 == 3, 3 < 6, 3虽然在3-6之间,但是存在于m_ids中
  3. 然后拿到版本链的下一个数据,事务ID为2,匹配上了第二条规则,此时我们就确定该数据的版本是可以访问的,因此此处ReadView得到的数据就是版本链中事务Id为2的数据:


2️⃣ 锁

MVCC是用于解决快照读则是用于解决当前读,通过加三种不同粒度的锁保证不出现幻读的现象,我们以更新数据作为一个例子:

当我们执行 update 语句时,实际上是会对记录加临键锁的,如果其他事务对持有临键锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放

如果我们根据主键更新了一条数据,就会给这条数据加上行锁,防止其他的事务进行修改,但是不会影响到对其他数据的修改:

如果,我们修改的条件是非索引字段,就会发生锁住全表,实际上是给整个范围加上了临键锁,并非是加上了表锁,因为它走了全表扫描

以上就是锁帮助我们去解决当前读的幻读问题,至此两种解决幻读的策略就回顾结束


3️⃣ MySQL幻读的发生

这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。当然,这是在可重复读(RR)隔离级别下。我们一起来看看发生幻读现象的场景:

废话不多说,我们直接上场景,现在我们已知有一张User表:

然后两个事务时间线按照下表从上到下展开事务:

事务A事务B
begin;begin;
select * from user where id = 5;
insert into user values(5,"日向",18)
commit;
update user set name = '影山' where id = 5;
select * from user where id = 5;

我们其实会发现事务A第一次读的数据为空,但是第二次读到的数据是有数据的,也就是说一次事务中出现了两次不一样的查询结果,也就是幻读

我们来分析一下原因:

  • 事务 A 第一次执行普通的 select 语句时生成了一个 ReadView
  • 之后事务 B 向表中新插入了一条 id = 5 的记录并提交。
  • 接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id
  • 之后事务 A 再使用普通 select 语句去查询这条记录时,就符合四条规则中的第一条,即上一个修改数据的事务是当前事务
  • 因此就可以看到这条记录了,于是就发生了幻读

那我们想要避开这种幻读的话,尽量在开启事务之后,马上执行当前读这类的语句,给记录加上临键锁,这样就不会被其他的事务所修改导致幻读。


💬 总结

在展开MySQL幻读的场景之前,我们首先通过例子回顾了一下事务的隔离性的实现,也就是MVCC和锁对于事务的贡献,顺便提到了Insert使用不当会导致全表锁,确确实实,这两种策略很大程度上解决了幻读的问题,但是也存在我们演示的个别例子,当然在最后我们也给出了解决的方法,就是在开启事务后今早的去执行当前读的语句。


🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

求赞.jpeg