欢迎关注公众号 【11来了】 ,持续 MyBatis 源码系列内容!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
不可重复读和幻读到底有哪些区别?
接下来聊聊 MySQL 中的脏读、不可重复读、幻读问题,重点讲清楚不可重复读和幻读之间的差别,以及 MySQL 如何解决幻读问题
脏读
-
说明: 脏读即事务 A 读取到了事务 B 已经修改但是没有提交的数据,此时如果事务 B 回滚,事务 A 读取的则为脏数据
-
对应隔离级别: 在 读未提交 隔离级别下会出现脏读问题
不可重复读
-
说明: 当事务内相同的记录被检索两次,在两次检索的中间,其他的事务新增、修改、删除了该记录,导致两次得到的结果不同。
-
对应隔离级别: 在 读已提交 隔离级别下会出现不可重复读问题
-
案例: 比如事务 A 第一次查询表中 id = 1 的用户年龄,查询出来为 26,此时事务 B 去修改 id = 1 的用户年龄为 27,那么事务 A 再次查询,发现年龄为 27,这就是不可重复读
事务 A | 事务 B |
---|---|
begin; | |
select age from user where id = 1; return 26; | begin; |
update user set age = 27 where id = 1; commit; | |
select age from user where id = 1; return 27; | |
commit; |
幻读
-
说明: 在事务 A 执行时,事务 B 新增加了记录,事务 A 读取到了事务 B 新增加的记录
-
对应隔离级别: 在 可重复读 隔离级别下会出现幻读问题,幻读是不可重复读的一种特殊情况
不可重复读和幻读的区别
- 不可重复读是事务 A 针对相同记录的两次查询结果不同,因此发生不可重复读是因为在事务 A 执行的过程中,事务 B 去 新增、修改、删除 了事务 A 读取的数据
- 幻读是事务 A 读取到了事务 B 新 新增 的数据,幻读是不可重复读中的一种特殊情况
在可重复读隔离级别下,解决了不可重复读的问题,但是还是会有幻读问题的存在
数据库标准
上边说的脏读、不可重复读、幻读都是数据库标准下,各个具体的数据库可以自定义的实现对应的事务隔离级别,在 MySQL 中,通过 MVCC 和临键锁可以在可重复读隔离级别下解决幻读的问题,接下来说一下 MySQL 如何解决幻读问题
MySQL 的查询操作分为了两种,在这两种查询操作中会通过 MVCC 和临键锁来解决幻读问题:
- 快照读:select …
- 通过 MVCC 解决幻读问题,这种方式在查询时会根据当前的事务 id,只读取在当前事务之前插入的数据,因此不会读取到其他事务新提交的数据,从而造成幻读
- 当前读:select … for update
- 通过临键锁解决幻读问题,在查询的数据行之间加锁,来避免其他事务向相关的数据行之间插入数据,从而造成幻读
进阶
至此,MySQL 的幻读问题就得到解决了,掌握了上边的内容也就足以应对面试了,但是在 MySQL 查询的一些极端场景下,还是会出现幻读问题,通过了解这样的极端场景,可以对 MVCC 和临键锁有一个更清晰的认识
如下两种情况:
1、情况 1 中事务 A 先进行查询操作,之后去更新数据,发现可以更新到其他事务新提交的数据
2、情况 2 中事务 A 先进行快照读,之后再进行当前读,发现可以读取到其他事务新提交的数据
这两种情况 本质原因 是更新数据和当前读时,都会去更新版本号,因此在 MVCC 机制下会读取到最新数据(读取到了其他事务新提交的数据),造成幻读
接下来详细介绍一下两种情况造成的幻读现象:
- 情况1:事务 A 通过更新操作获取最新视图之后,可以读取到事务 B 提交的数据,出现幻读现象
对于下图中的执行顺序,会出现幻读现象,可以看到在事务 A 执行到第 7 行发现查询到了事务 B 新提交的数据了
这里都假设使用的 InnoDB 存储引擎,事务隔离级别默认都是 可重复读
在可重复读隔离级别下,使用了 MVCC 机制,select 操作并不会更新版本号,是快照读(历史版本),执行 insert、update 和 delete 时会更新版本号,是当前读(当前版本),因此在事务 A 执行了第 6 行的 update 操作之后,更新了版本号,读到了 id = 5 这一行数据的最新版本,因此出现了幻读!
(这里的版本号其实就是针对 MVCC 机制中的数据版本链)
- 情况2:事务 A 在步骤 2 执行的读操作并不会生成间隙锁,因此事务 B 会在事务 A 的查询范围内插入行
对于下边这种情况也会出现幻读,在第 6 行使用 select ... for update
进行查询,这个查询语句是当前读(查询的最新版本),因此查询到了事务 B 新提交的数据,出现了幻读!
那么对于以上两种情况来说,为什么会出现幻读呢?
对于事务 A 出现了幻读,原因就是,事务 A 执行的第 2 行是普通的 select 查询,这个普通的 select 查询是快照读,不会生成临键锁(具体生成临键锁、记录锁还是间隙锁根据 where 条件的不同来选择),因此就 不会锁住这个快照读所覆盖的记录行以及区间
那么事务 B 去执行插入操作,发现并没有生成临键锁,因此直接可以插入成功
重要:那么我们从代码层面尽量去避免幻读问题呢?
上边说的幻读问题其实就是 事务 A 在查询的时候没有查到其他事务提交的数据,但是在修改数据或当前读时,由于更新了版本号,因此可以查询到其他事务提交的数据 ,这种幻读现象可以在事务开启时通过 for update 直接加锁来解决:
- 通过 for update 加锁来阻塞,避免因为幻读,更新到其他事务提交的数据
在一个事务开始的时候,尽量先去执行 select ... for update
,执行这个当前读的操作,会先去生成锁(具体生成记录锁、临键锁还是表锁要根据 where 条件来判断),锁住查询记录的区间,不会让其他事务插入新的数据,因此就不会产生幻读
这里我也画了一张图如下,你也可以去启动两个会话窗口,连接上 mysql 执行一下试试,就可以发现,当事务 A 执行 select ... for update
操作之后,就会加上临键锁(由于 where 后的条件是 id=5,对主键索引进行等值查询,因此给 id=5 这一行的数据添加记录锁),那么事务 B 再去插入 id=5 这条数据,就会因为有锁的存在,阻塞插入语句