MySql系列:MySQL 到底是怎么解决幻读的?

愿天堂没有BUG

共 3115字,需浏览 7分钟

 · 2021-08-19

前言

寻找光的过程,就是向上爬的过程

纯真的【苏醒】、海上的【告别】、傍晚的【遗憾】、山中的【约定】、平凡的【妥协】、自我的【同行】、美好的【相遇】、社恐的【停顿】、表达的【倾听】、勇敢的【探索】,最终发现,【爱】是雾中的灯塔,指引着出海时的彼岸,也是向往的归宿。所以,“幼鸟”们,当落定所有声色,也别忘记哼着歌。——幼鸟指南

什么是幻读?在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多出来或者少的哪一行被叫做幻行。

为什么要解决幻读?在高并发数据库系统中,需要保证事务与事务之间的隔离性,还有事务本身的一致性。

MySQL 是如何解决幻读的?如果你看到了这篇文章,那么我会默认你了解了脏读 、不可重复读与可重复读。


😎什么是幻读,脏读,不可重复读呢?

  • 事务 A、B 交替执行,事务 A 被事务 B 干扰到了,因为事务 A 读取到事务 B 未提交的数据,这就是脏读。

  • 在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。

  • 事务 A 查询一个范围的结果集,另一个并发事务 B 往这个范围中插入 / 删除了数据,并静悄悄地提交,然后事务 A 再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。


🎊不可重复读(锁行即可解决)和幻读的区别(需要锁表解决)

很多人容易搞混不可重复读(不可重复读重点是在 update,即事务前后对比特定数据内容的修改;而幻读是 insertdelete,即事务前后数据条数的对比)和幻读,确实这两者有些相似。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

但是 MySQL、ORACLE、PostgreSQL 等成熟的数据库,出于性能考虑,都是使用以乐观锁为理论基础的 MVCC(多版本并发控制) 来避免这两种问题。


🎎悲观锁和乐观锁

传送门:乐观锁和悲观锁

  • 悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

  • 乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。


🎇1. 多版本并发控制(MVCC)(快照读/一致性读)

多数数据库都实现了多版本并发控制,并且都是靠保存数据快照来实现的。以 InnoDB 为例,每一行中都冗余了两个字断。

一个是行的创建版本,一个是行的删除(过期)版本。具体的版本号(trx_id)存在  information_schema.INNODB_TRX 表中。版本号(trx_id)随着每次事务的开启自增。

事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。

普通的 select 就是快照读。

SELECT id FROM T WHERE number = 1;
复制代码

原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。


🎈2. next-key 锁 (当前读)

next-key 锁包含两部分:

  • 记录锁(行锁)

  • 间隙锁

记录锁是加在索引上的锁,间隙锁是加在索引之间的。(思考:如果列上没有索引会发生什么?)

SELECT id FROM T WHERE number = 1 for update;
SELECT id FROM T WHERE number = 1 lock in share mode;
insert
update
delete
复制代码

原理:将当前数据行与上一条数据和下一条数据之间的间隙锁定,保证此范围内读取的数据是一致的。


🤳其他:MySQL InnoDB 引擎 RR 隔离级别是否解决了幻读

github 上面的评论 地址:

Mysql官方给出的幻读解释是:只要在一个事务中,第二次select多出了row就算幻读。
a事务先select,b事务insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放(释放后a事务可以随意dml操作),a事务再select出来的结果在MVCC下还和第一次select一样,接着a事务不加条件地update,这个update会作用在所有行上(包括b事务新加的),a事务再次select就会出现b事务中的新行,并且这个新行已经被update修改了,实测在RR级别下确实如此。
如果这样理解的话,Mysql的RR级别确实防不住幻读

有道友回复 地址:

在快照读读情况下,mysql通过mvcc来避免幻读。在当前读读情况下,mysql通过next-key来避免幻读。select * from t where a=1;属于快照读 select * from t where a=1 lock in share mode;属于当前读 不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以我认为mysql的rr级别是解决了幻读的。

先说结论,MySQL 存储引擎 InnoDB 隔离级别 RR 解决了幻读问题。面试问烂的 MySQL 四种隔离级别,建议大家多了解多学习。

如引用一问题所说,T1 select 之后 update,会将 T2insert 的数据一起更新,那么认为多出来一行,所以防不住幻读。看着说法无懈可击,但是其实是错误的,InnoDB 中设置了快照读和当前读两种模式,如果只有快照读,那么自然没有幻读问题,但是如果将语句提升到当前读,那么 T1select 的时候需要用如下语法:select * from t for update (lock in share mode)  进入当前读,那么自然没有 T2 可以插入数据这一回事儿了。


👏注意

next-key 固然很好的解决了幻读问题,但是还是遵循一般的定律,隔离级别越高,并发越低。


作者:Sunny_Chen
链接:https://juejin.cn/post/6996081766037471239
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报