1、关于幻读的几个问题
幻读是答辩和面试的热点考点,然而幻读本身的定义非常模糊,关于幻读的解读也是人云亦云,网上有各种版本,很多人对幻读也是似懂非懂。如果你不确定自己是否真正理解了幻读,不妨先问自己以下几个问题:
1、什么是幻读,幻读和不可重复读的区别是什么?
2、InnoDB RR隔离级别是怎么避免幻读?MVCC还是next-key locks?
3、Mysql的可重复读级别真的能解决幻读吗?
2、什么是幻读
2.1 幻读的定义
Mysql官网:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a select is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row
维基百科:
在事务执行过程中,当两个完全相同的查询语句执行得到不同的结果集。这种现象称为“幻影读(phantom read)”
2.2 幻读和不可重复读的区别
不可重复读维基百科:
在一次事务中,当一行数据获取两遍得到不同的结果表示发生了“不可重复读”.
同一个事务执行过程中,另外一个事务提交了新数据,因此本事务先后两次读到的数据结果会不一致;
幻读指两个完全相同的查询语句执行得到不同的结果集,侧重结果集(sets of rows)不同,而不可重复读强调两次读的结果不一致。幻读是不可重复读的一种特殊场景¹(出自维基百科)。
【注】广义的讲,幻读是不可重复读的一种特例;
3、InnoDB RR隔离级别下怎么避免幻读
Mysql怎么避免幻读,《高性能的Mysql》说是MVCC解决幻读,《深入Mysql InnoDB存储引擎》上面说是next-key locking解决了幻读。到底是MVCC解决了幻读还是next-key lock解决了幻读呢?
要回答这个问题,我们看看下面几个场景:
首先我们创建一张child表,插入90、102数据,并且讲事务隔离级别设置read committed;
create` `table` ``child` (`` ```id` ``int` `(11)``);``insert` `into` ``child` (`id`) ``values``(``'90'``);``insert` `into` ``child` (`id`) ``values``(``'102'``);
set` `global` `transaction` `isolation` `level` `read` `committed``;
Case1:当前读
t1 | t2 | |
---|---|---|
1 | begin; | |
2 | SELECT * FROM child WHERE id > 100 FOR UPDATE; +—–+ | id | +—–+ | 102 | +—–+ | begin ; |
3 | insert child into (id) values(101);(RR级别下这里为阻塞住) | |
4 | commit; | |
5 | SELECT * FROM child WHERE id > 100 FOR UPDATE; +—–+ | id | +—–+ | 101 | | 102 | +—–+ | |
6 | commit; |
在RC隔离级别下,t1在第5步的时候读取到101这个幻影,出现了幻读;
InnoDB用next-key locking算法避免幻读(To prevent phantoms, InnoDB
uses an algorithm called next-key locking that combines index-row locking with gap locking.);
next-key locking 相关见 InnoDB Locking
Case 2:快照读
t1 | t2 | |
---|---|---|
1 | begin; | |
2 | SELECT * FROM child WHERE id > 100; +—–+ | id | +—–+ | 102| +—–+ | begin ; |
3 | insert child into (id) values(101); | |
4 | commit; | |
5 | SELECT * FROM child WHERE id > 100; +—–+ | id | +—–+ | 102 | +—–+ | |
6 | commit; |
在RC隔离级别下,事务t1在第2步和第5步快照读,读取的结果一样,没有出现幻读,这里避免幻读的主要是依赖Mysql MVCC机制;
每个事务读到的数据项都是一个历史快照(snapshot)并依赖于实现的隔离级别。写操作不覆盖已有数据项,而是创建一个新的版本,直至所在操作提交时才变为可见。
MVCC详见:InnoDB Multi-Versioning (待补充)
Case3:快照读+当前读
t1 | t2 | |
---|---|---|
1 | begin; | |
2 | SELECT * FROM child WHERE id > 100; +—–+ | id | +—–+ | 102 | +—–+ | begin ; |
3 | insert child into (id) values(101); | |
4 | commit; | |
5 | SELECT * FROM child WHERE id > 100 FOR UPDATE; +—–+ | id | +—–+ | 101 | | 102 | +—–+ | |
6 | commit; |
事务t1在第2步和第5步当前读,读取到101这个幻影,那这个算不算幻读的范畴呢?
现在大家比较容易的接受的理解一次当前读和一次快照读不属于幻读的范畴,
不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用
结论:
1、在快照读读情况下,mysql通过mvcc来避免幻读。 2、在当前读读情况下,mysql通过next-key来避免幻读。
- select * from t where a=1;属于快照读
- select * from t where a=1 lock in share mode;属于当前读
3、不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以我认为mysql的rr级别是解决了幻读的。
4、总结
1、幻读指两个完全相同的查询语句执行得到不同的结果集,侧重结果集(sets of rows)不同,而不可重复读强调两次读的结果不一致。幻读是不可重复读的一种特殊场景;
2、ANSI SQL-92标准下,RR级别是允许幻读;但是Mysql InnoDB的实现没有完全遵循ANSI SQL-92的标准,通过next-key lock避免了幻读;
3、Mysql InnoDB在快照读读情况下,通过mvcc来避免幻读;在当前读读情况下,通过next-key lock来避免幻读;
4、我们认为快照读和当前读混合的情况,因为类似幻读读现象不属于幻读读范畴。
参考文档:
https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html
https://github.com/Yhzhtk/note/issues/42
http://mysql.taobao.org/monthly/2017/06/07/
https://zh.wikipedia.org/wiki/%E4%BA%8B%E5%8B%99%E9%9A%94%E9%9B%A2
https://github.com/Yhzhtk/note/issues/42
http://mysql.taobao.org/monthly/2017/06/07/