关于如何去防止幻读,从MVCC和锁的方式解决快照读和当前读的幻读问题是一个办法,可事实上这两个解决方案在很大程度上解决了幻读现象,但还是会有个别情况的幻读问题无法解决,那么在今天的文章中,我们将以以下几点讲述一下MySQL幻读相关的知识:
1 首先回顾一下快照读是如何避免幻读的
2 然后回顾一下当前读是如何避免幻读的
3 MySQL中幻读发生的场景与现象
MVCC
在介绍MySQL幻读发生之前,我们首先简单的回顾一下MySQL是如何解决幻读的(只做简单解读)
针对于快照读,MySQL是通过MVCC解决了幻读,这个我们在讲解MVCC的时候就提到,MVCC是用于解决快照读的幻读问题的,而MVCC的具体实现又依赖于数据库记录的三个隐式字段、undo log日志、readView。
每次事务开始后,进行快照读,也就是第一个查询语句的时候,会生成一个ReadView,ReadView中包含的字段有:当前活跃的事务ID、最小的事务ID、最大事务ID+1、创建者的事务ID,而每次操作后数据都会记录在undo log中,会形成一个undo log版本链,记录的数据中含有的隐藏字段事务ID、回滚指针又是我们判断的主要依据。说了这么多,我们简单举个栗子:
首先第一个快照读,我们简单分析一下它生成的ReadView,此时尚未提交的事务为3,4,5,因此活跃事务ID集合[3,4,5],其中最小的为3,最大的加一为6,因此最小事务ID = 3,最大事务ID+1 = 6,创建这个快照读的事务为5,因此创建事务ID = 5:
根据它的操作我们可以得到它的undo log版本链如下:
现在我们就要借用这张图和四个规则开始匹配,根据已知的数据,我们可以把规则中的数据填上方便我们进行匹配:
从版本链的最新的数据开始遍历:
- 首先事务ID为4,发现四条规则都匹配不上:4 != 5, 4 > 3, 4 < 6, 4虽然在3-6之间,但是存在于m_ids中
- 然后拿到版本链的下一个数据,事务ID为3:3 != 5, 3 == 3, 3 < 6, 3虽然在3-6之间,但是存在于m_ids中
- 然后拿到版本链的下一个数据,事务ID为2,匹配上了第二条规则,此时我们就确定该数据的版本是可以访问的,因此此处ReadView得到的数据就是版本链中事务Id为2的数据:
锁
MVCC是用于解决快照读,锁则是用于解决当前读,通过加三种不同粒度的锁保证不出现幻读的现象,我们以更新数据作为一个例子:
当我们执行 update 语句时,实际上是会对记录加临键锁的,如果其他事务对持有临键锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放。
如果我们根据主键更新了一条数据,就会给这条数据加上行锁,防止其他的事务进行修改,但是不会影响到对其他数据的修改:
如果,我们修改的条件是非索引字段,就会发生锁住全表,实际上是给整个范围加上了临键锁,并非是加上了表锁,因为它走了全表扫描:
以上就是锁帮助我们去解决当前读的幻读问题,至此两种解决幻读的策略就回顾结束
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使用不当会导致全表锁,确确实实,这两种策略很大程度上解决了幻读的问题,但是也存在我们演示的个别例子,当然在最后我们也给出了解决的方法,就是在开启事务后今早的去执行当前读的语句。