Description
问题引出
我之前的一篇博客 数据库并发不一致分析 有提到过事务隔离级别以及相应加锁方式、能够解决的并发问题。
标准情况下,在 RR(Repeatable Read) 隔离级别下能解决不可重复读(当行修改)的问题,但是不能解决幻读的问题。
而之前有看过一篇 mysql 加锁的文章 MySQL 加锁处理分析,里面有提到一点:
对于Innodb,Repeatable Read (RR) 针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。
那么问题来了,到底 Innodb 中 RR 隔离级别是否能解决幻读呢?
在 MySQL 加锁处理分析这篇文章下面的评论中,有这样的一个交流:
ontheway
弱弱地问一句,我看的书里面都说的是RR隔离级别不允许脏读和不可重复读,但是可以幻读,怎么和作者说的不一样呢?hedengcheng(作者)
你说的没错,因此我在文章一开始,就强调了这一点。mysql innodb引擎的实现,跟标准有所不同。
求证官方文档
MySQL Innodb 引擎的实现,跟标准有所不同,针对这个问题,我表示怀疑,于是查看 mysql 官方文档关于 RR的解释,里面有这么一段话:
For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range.
大致意思就是,在 RR 级别下,如果查询条件能使用上唯一索引,或者是一个唯一的查询条件,那么仅加行锁,如果是一个范围查询,那么就会给这个范围加上 gap 锁或者 next-key锁 (行锁+gap锁)。
从这句话的理解来看,和文章里的解释一样,由于 RR 级别对于范围会加 GAP 锁,这个和 sql 的标准是有一些差异的。
其他解释
后面又发现了一篇文章 Understanding InnoDB transaction isolation levels,文章中又提到:
This isolation level is the default for InnoDB. Although this isolation level solves the problem of non-repeatable read, but there is another possible problem phantom reads.
大概意思是,RR 能解决不可重复读的问题,但仍可能发生幻读,怀疑作者并不了解 Innodb 的特殊实现,评论中也有提到:
Do you mean 'write skew' instead of 'phantom reads'? The 'repeatable read' in SQL standard allows 'phantom reads', however, since InnoDB uses next-key locking this anomaly does not exist in this level. Looks like it's equivalent to 'snapshot isolation' in Postgres and Oracle.
再来看一篇文章 MySQL的InnoDB的幻读问题,这里面提供了一些例子,还没来得及分析,但最后的结论是:
MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。
最终结论
Innodb 的 RR 隔离界别对范围会加上 GAP,理论上不会存在幻读,但是是否有例外呢,这个还需要进一步求证。
Activity
hexufeng commentedon Jan 25, 2017
很好的总结,谢谢。很多人由于看到innodb的RR级别不发生幻读,所以就以为sql标准中RR是不发生幻读的,甚至有些搞培训的都这么认为!
xiaoma20082008 commentedon Mar 22, 2017
maybe you try it like this

fqdeng commentedon Apr 17, 2017
@xiaoma20082008
所以这样还是没法避免 幻读的情况对吧,
但是之前博主的博客,不是说 select * from t where a = 1;
虽然a不是主键,也不存在唯一索引,但是会对a 符合条件的行 加上gap锁么?
这样事务2应该会阻塞,而不是update成功才对,事务1也不会读到事务2的更新才是
lc87624 commentedon Jun 2, 2017
@jonwinters,@xiaoma20082008 举的例子中,第一次的"select * from t where a = 1"是快照读,而不是当前读,所以是不会加锁的,事务2也不会阻塞。但是我认为这个例子反映的并不是幻读的问题。
引用MySQL 加锁处理分析里的定义:
幻读对比的是两次当前读返回的结果,@xiaoma20082008 例子里对比的是一次快照读和一次当前读,这个不是幻读的问题。
fqdeng commentedon Jun 5, 2017
@lc87624
事务里面有快照读跟当前读,soga
我一直没搞清楚这个问题,刚才看了mvcc搞懂了 多谢,,
liuxiaoyu8858 commentedon Jun 26, 2017
MVCC是实现的是快照读,next-key locking 是对当前读 都可以避免幻读
hao5ang commentedon Sep 19, 2017
感觉这篇文章讲的不错: http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
Innodb 要想避免幻读, 需要加锁读.
"如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。"
ChenHaoyuan commentedon Sep 24, 2018
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级别确实防不住幻读
nvidi commentedon Dec 24, 2018
在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key来避免幻读。
select * from t where a=1;属于快照读
select * from t where a=1 lock in share mode;属于当前读
不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以我认为mysql的rr级别是解决了幻读的。
MAGE001 commentedon Jan 20, 2019
多读出的一行,是因为 "MVCC快照读中,“自己的修改可见”。"还是update的锁把快照读变成了当前读。
ghost commentedon Feb 16, 2019
同意 @nvidi 的说法。楼上有几个例子中的两次查询根本就不一样的啊。
dunixd commentedon Mar 25, 2019
MVCC快照读本身就包括两部分可见:1、事务开始前已提交的可见 2、自己本事务的修改可见
因此这里应该是本身快照读不可见的记录,由于自己修改过变成了快照读可见
kcruci commentedon Mar 26, 2019
幻读包括count行数这种吗?
31 remaining items