Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Innodb 中 RR 隔离级别能否防止幻读? #42

Open
Yhzhtk opened this issue Apr 14, 2016 · 41 comments
Open

Innodb 中 RR 隔离级别能否防止幻读? #42

Yhzhtk opened this issue Apr 14, 2016 · 41 comments

Comments

@Yhzhtk
Copy link
Owner

Yhzhtk commented Apr 14, 2016

问题引出

我之前的一篇博客 数据库并发不一致分析 有提到过事务隔离级别以及相应加锁方式、能够解决的并发问题。

标准情况下,在 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,理论上不会存在幻读,但是是否有例外呢,这个还需要进一步求证。

@hexufeng
Copy link

很好的总结,谢谢。很多人由于看到innodb的RR级别不发生幻读,所以就以为sql标准中RR是不发生幻读的,甚至有些搞培训的都这么认为!

@xiaoma20082008
Copy link

maybe you try it like this
rr

@fqdeng
Copy link

fqdeng commented Apr 17, 2017

@xiaoma20082008
所以这样还是没法避免 幻读的情况对吧,
但是之前博主的博客,不是说 select * from t where a = 1;
虽然a不是主键,也不存在唯一索引,但是会对a 符合条件的行 加上gap锁么?
这样事务2应该会阻塞,而不是update成功才对,事务1也不会读到事务2的更新才是

@lc87624
Copy link

lc87624 commented Jun 2, 2017

@jonwinters,@xiaoma20082008 举的例子中,第一次的"select * from t where a = 1"是快照读,而不是当前读,所以是不会加锁的,事务2也不会阻塞。但是我认为这个例子反映的并不是幻读的问题。
引用MySQL 加锁处理分析里的定义:

所谓幻读,就是同一个事务,连续做两次当前读 (例如:select * from t1 where id = 10 for update;),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。

幻读对比的是两次当前读返回的结果,@xiaoma20082008 例子里对比的是一次快照读和一次当前读,这个不是幻读的问题。

@fqdeng
Copy link

fqdeng commented Jun 5, 2017

@lc87624
事务里面有快照读跟当前读,soga
我一直没搞清楚这个问题,刚才看了mvcc搞懂了 多谢,,

@liuxiaoyu8858
Copy link

MVCC是实现的是快照读,next-key locking 是对当前读 都可以避免幻读

@hao5ang
Copy link

hao5ang commented Sep 19, 2017

感觉这篇文章讲的不错: http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
Innodb 要想避免幻读, 需要加锁读.
"如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。"

@ChenHaoyuan
Copy link

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.
原文

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
Copy link

nvidi commented 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
Copy link

MAGE001 commented Jan 20, 2019

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.
原文

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级别确实防不住幻读

多读出的一行,是因为 "MVCC快照读中,“自己的修改可见”。"还是update的锁把快照读变成了当前读。

@ghost
Copy link

ghost commented Feb 16, 2019

同意 @nvidi 的说法。楼上有几个例子中的两次查询根本就不一样的啊。

@dunixd
Copy link

dunixd commented Mar 25, 2019

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.
原文

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级别确实防不住幻读

多读出的一行,是因为 "MVCC快照读中,“自己的修改可见”。"还是update的锁把快照读变成了当前读。

MVCC快照读本身就包括两部分可见:1、事务开始前已提交的可见 2、自己本事务的修改可见
因此这里应该是本身快照读不可见的记录,由于自己修改过变成了快照读可见

@kcruci
Copy link

kcruci commented Mar 26, 2019

幻读包括count行数这种吗?

@hzj629206
Copy link

幻读包括count行数这种吗?

select count() from table where condition for update 这种会。
select count(
) from table where condition for update和select count(*) from table where condition的结果可能不一样。

@louishust
Copy link

louishust commented Jul 27, 2019

发表点个人看法,先看定义

P3 (Phantom): Transaction T1 reads a set of data items satisfying some . Transaction T2 then creates data items that satisfy T1’s and commits. If T1 then repeats its read with the same , it gets a set of data items different from the first read.(A Critique of ANSI SQL Isolation Levels)

ANSI对于隔离级别的定义其实并没有针对具体的实现,所谓的快照读和当前读,是针对MVCC多版本这种特定实现技术的。比如对于单版本就不存在什么快照读了。

InnoDB RR隔离级别下的READ-ONLY事务,是能保证不可重复读和消除幻读的,因为MVCC。但是InnoDB的RR隔离级别存在P4(Lost update)现象,即更新操作自动降级到了RC级别,即更新是在最新版本的数据上进行,故RR隔离级别下的非READ-ONLY事务,可能存在@xiaoma20082008 描述的情况,即存在不可重复读和幻象的情况。

为了保证InnoDB的RR隔离级别达到ANSI-RR级别,对于单纯的SELECT语句,需要显示for update进行gap 锁,更新语句自动会上gap锁,防止其它事务的干扰,这样就能达到可重复读和消除幻读的目的。

InnoDB为什么实现的RR存在P4现象?因为商用系统为了高并发。而Jim Gray们针对MVCC实现定义的Snapshot Isolation,其实是不允许P4现象的,出现更新冲突,遵循first-commiter-win的原则,其它事务需要回滚。

所以InnoDB的RR隔离级别是大于RC级别,小于Snapshot隔离级别。但是又与传统ANSI-RR的定义有差异(Innodb-RR解决了A3幻象,但是没禁止更新丢失、写倾斜的问题,ANSI-RR定义禁止了更新丢失以及读写倾斜,但是允许幻象)。

https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf
https://www.percona.com/blog/2012/03/27/innodbs-gap-locks/

最后给出一个DDIA书中的隔离级别测试链接(其中INNODB RR下的R/O表示只读模式下才生效!!!):
https://github.com/ept/hermitage

DBMS So-called isolation level Actual isolation level G0 G1a G1b G1c OTV PMP P4 G-single G2-item G2
PostgreSQL "read committed" ★ monotonic atomic view
"repeatable read" snapshot isolation
"serializable" serializable
MySQL/InnoDB "read uncommitted" read uncommitted
"read committed" monotonic atomic view
"repeatable read" ★ monotonic atomic view R/O R/O
"serializable" serializable
Oracle DB "read committed" ★ monotonic atomic view
"serializable" snapshot isolation some
MS SQL Server "read uncommitted" read uncommitted
"read committed" (locking) ★ monotonic atomic view
"read committed" (snapshot) monotonic atomic view
"repeatable read" repeatable read some
"snapshot" snapshot isolation
"serializable" serializable
FDB SQL Layer "serializable" ★ serializable

@zhangzui
Copy link

我觉得 RR级别下,没有严格的做到解决幻读问题!

@zhangzui
Copy link

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

其实这和事务中,先查,再修改记录,然后再查,结果出现不一致,是差不多的场景,但是在RR级别下,同一个事务内,应该不能叫脏读!

#为啥只要 进行了update 等加X锁操作,就会使得MVCC版本失效,或者变化呢 ,求大神解答?
猜想:只要获取了记录的行锁,mysql innodb 自动会将当前操作完的版本,和对应的变更刷新到最新的版本上。再次查询 会获取最新的行记录

@kylexlau
Copy link

http://mysql.taobao.org/monthly/2017/06/07/

看看这篇。

@JasonLiuLiuLiuLiu
Copy link

还有美团这边的这篇文章 https://tech.meituan.com/2014/08/20/innodb-lock.html

@IamNotShady
Copy link

总结的不错

@northhurricane
Copy link

在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key来避免幻读。
select * from t where a=1;属于快照读
select * from t where a=1 lock in share mode;属于当前读

不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以我认为mysql的rr级别是解决了幻读的。

快照读和当前读不是SQL标准定义的内容,是为了更好的理解MVCC实现提出来的。如果按照ChenHaoyuan给出的例子,事务A如果设置了serializable的隔离级,事务B是不能做任何insert/update的。这是标准的避免幻读的做法。
对于幻读,不止是select,update的可见范围也要和select一致,在MySQL的RR模式下,显然做不到这一点,所以不是严格的避免了幻读。

@sanwancoder
Copy link

还有美团这边的这篇文章 https://tech.meituan.com/2014/08/20/innodb-lock.html

感谢

@loniecc
Copy link

loniecc commented May 4, 2020

感觉是幻读的问题定义让大家产生了分歧啊

@MAGE001
Copy link

MAGE001 commented May 31, 2020

很久了,大家居然还在讨论。前几天看《MySQL技术内幕-Innodb引擎》里面提到了,MySQL跟标准RR不一样,标准RR存在幻读问题,但innodb通过next-key-lock解决了RR的幻读问题

@aLibeccio
Copy link

看了半天越来越搞不懂幻读的定义了.......

@hezhijie
Copy link

刚刚看到这个问题,现在是2020年7月31号,发现这个问题的创建日期是2016年4月14号。4年了问题还在讨论无非是对mysql里“幻读” 的解读不一致。 两种理解方式其实的都可以找到答案。

  • 第一种解读
    select * from table where index =1;
    这种不加锁的方式没有幻读的解决办法,其实是通过mvcc 一致性非锁定读 的方式解决的,读到的总是事务开始前找到的快照。 有点java 里thread local的感觉, 一次请求进来先把结果和条件保留下来,再次遇到同样的条件直接取出,事务退出后清空。

  • 第二种解读
    select * from table where index =1 for update;
    这种是通过加行所或间隙锁的方式实现,也可以说是next key lock, 一个事务先执行并对数据加了锁,再有其他的事务进来会先检查锁的状态,类似java 里监视器的感觉,先检查监视器是否有持有对象是否是自己。 一旦发现有其他事务持有当前要访问的记录就开始阻塞,等待锁的释放 然后开始竞争

@ruiwu-bupt
Copy link

ruiwu-bupt commented Oct 27, 2020

查到一点资料供参考。之前有人反馈过这个 https://bugs.mysql.com/bug.php?id=63870,不过mysql的人表示就是这么设计的,这里有记录https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html。
从文档里可以看到他们认为连续的快照读或者连续的当前读出现数据不一致才符合幻读的定义,而这里出现问题的是先快照读然后当前读,所以他们是这么说的:To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking。
对于连续的快照读,mvcc会保证其他事务的修改在当前事务不可见;对于连续的当前读,第一个当前读会加间隙锁,别的事务要修改直接就阻塞了。在一个事务里先快照读再当前读,由于第一个快照读mvcc没有加锁,其他事务可以修改并提交,后面的当前读在设计上就可以读到已提交的事务,update之后状态变成了自己的修改,mvcc里自己的修改是可见的,这条记录就完全可见了。
总的来说这是一个他们设计好的feature。https://developer.aliyun.com/article/698749 这里也有一点解释这么设计的原因。

@haxianhe
Copy link

1.事务的隔离级别定义的的是当前读。
2.InnoDB在可重复读级别下的当前读通过Next-Key Lock锁机制解决了幻读问题。
3.InnoDB在可重复读级别下的快照读存在“幻读”问题,解决方案是(1)改为可串行化隔离级别(2)改为当前读(读操作加锁)

@prprprus
Copy link

prprprus commented Apr 16, 2021

同意 @ChenHaoyuan 评论里提到的对于幻读的定义:在一个未提交的事务中,突然会多出若干条记录,就算幻读

做了一个实验

预备

操作系统版本(Mac 装的虚拟机):

MySQL 的版本:

测试表结构:

初始测试数据:

最后开两个 MySQL 客户端终端,用来模拟两个独立的事务进程,下面简称 T1 和 T2

步骤

  1. T1 和 T2 分别开启事务:

  1. T2 插入一条新记录并 COMMIT

  1. 此时 T1 并不能读取到 id=13 这条记录(注意:无论 T2 是否已经提交事务,T1 目前都无法读取到 id=13 这条记录)

  1. T1 将所有的 name 都改成 'z',然后再次读取整个表:

可以看到,T2 新插入的 id=13 也被修改到了,并且第二次可以读取到新插入的 id=13,也就是说发生了幻读

结论

至少对于 MySQL 5.7.33 版本来说,RR 隔离级别下是有可能发生幻读的

@ghost
Copy link

ghost commented May 12, 2021

1.事务的隔离级别定义的的是当前读。
2.InnoDB在可重复读级别下的当前读通过Next-Key Lock锁机制解决了幻读问题。
3.InnoDB在可重复读级别下的快照读存在“幻读”问题,解决方案是(1)改为可串行化隔离级别(2)改为当前读(读操作加锁)

事务的隔离级别如上所说,是 ANSI 标准,MySQL 是对标准定义的事务隔离级别进行实现。
「当前读」和「快照读」是 MySQL 用于实现事务隔离级别 MVCC 机制中衍生出的概念,而非 “事务的隔离级别定义的的是当前读” 。

RR 级别下存在幻读问题:

事务 A 事务 B
begin  
  begin
  insert into t1 values(2,2)
  commit
select * from t1 此处查询使用的是 A 开始时的快照读  
update t1 set a=100 此处更新使用的是当前读,B 已提交的数据也被读到,产生了幻读  
select * from t1 此处可以查询到 B 插入的数据  

「快照读」和「当前读」一起使用时就会出现幻读问题。

解决幻读问题,需要 next-key lock :

事务 A 事务 B
begin  
select * from t1 for update 此处为了避免幻读,使用 for update 加 next-key lock  
  begin
  insert into t1 values(3,3) 此处由于 A 已经加了 next-key lock ,所以此时的插入会被阻塞,等待 A 事务执行完毕释放锁以后才可以插入新的数据
commit A 事务提交,insert 可以执行
  commit

按照人对于概念的第一直觉,「RR 级别解决了幻读」应该体现为在 RR 级别下无需任何操作即可避免幻读,就像 Serializable 级别不论怎么执行事务都不会有幻读。
而实际上 MySQL 的 RR 级别并没有直接解决幻读,而是需要按照实际情况显式加锁去解决,这与直觉不符。
所谓「当前读」和「快照读」是 MySQL 自己的实现,而非标准定义的概念,混淆两者难免对初学者造成误导。
至于 “事务的隔离级别定义的的是当前读” 这种描述只能认为是宣传 MySQL 一种话术了。

@ytrlmy521
Copy link

如果就MySQL的事务隔离级别针对的是当前读,那么可以说是RR隔离级别下是解决了幻读,但是你要是理解的角度是快照度下出现的结果不一致理解成幻读我觉得也是可以的,看个人理解吧。

@gaoooyh
Copy link

gaoooyh commented Sep 28, 2021

MySQL InnoDB RR隔离级别

  1. 如果事务是只读不加锁, 通过MVCC 解决幻读
  2. 如果事务是读写都加锁, 相当于serializable隔离级别, 通过next-key 解决幻读
  3. 如果事务是读不加锁和写(写自动加锁), 相当于RC和RR的混合, select 前后其他事务插入数据, 返回结果一致, 而使用update, update 会加锁并采用当前读, (就相当于是read-committed), 它会将插入的数据的trx_id 改成当前事务的trx_id, 从而使普通的select 能够查询到其他事务插入的行;

92-SQL中定义的四个隔离级别和三种问题, 是采用文字描述的方式解释的, 没有具体的例子, 一千个人眼里有一千个哈姆雷特, 同一句话理解不同, 结论就不一样;

了解RR存在的问题, 以及在实际开发应用中, 是否会有相应的场景&如何避免, 才是我们更该去考虑的

@ghost
Copy link

ghost commented Sep 28, 2021

「幻读」这个概念,是 SQL 标准定义的,定义非常简单,执行事务时出现幻影行就是幻读。
它没有额外补充说明事务是以什么样的条件去读写,也没必要脑补它有什么附加的条件。
InnoDB 的 RR 隔离级别下,它没有完全解决掉「执行事务时出现幻影行」的现象,就不能说它解决了幻读。

当然,InnoDB 的 RR 隔离级别下,满足读写的某些条件就可以「避免幻影行」,这些条件下的「避免幻影行」最多是「幻读」定义的子集,都用「幻读」来描述很明显会混淆概念。

如果没有深入研究过 MySQL 原理的人看到「InnoDB 的 RR 隔离级别解决了幻读」,直觉就会理解为随便怎么操作都不会出现幻读。这样的理解,很可能导致不会去显式加锁,对于实际开发来说,就是一种误导。
如果都是一步理解到位,直接理解到 MySQL 底层的实现,这个讨论也就不会出现了。

概念的定义应该是非常严谨的,并不存在「个人理解」的说法,就像 HTTP ,可以理解到是 HTTP 协议,而不是像 Nginx 关于 HTTP 协议的什么实现。
如果编程时存在用「个人理解」来解释的概念,两个人理解的可能根本不是一个东西,交流成本直线上升。

@kebukeYi
Copy link

kebukeYi commented Dec 6, 2021

现在是 2021年12月6日21:47:52 还在纠结

@freephenix
Copy link

freephenix commented Feb 10, 2022

感谢讨论,最大的收获是引导了我去学习下MVCC中关于”当前读“和”快照读“的精彩实现

@DannyHoo
Copy link

InnoDB默认的事务隔离级别是repeatable read(后文中用简称RR),它为了解决该隔离级别下的幻读的并发问题,提出了LBCC和MVCC两种方案。其中LBCC(基于锁的并发控制,Record Locks、Gap Lock、Next-Key Lock)解决的是当前读情况下的幻读,MVCC解决的是普通读(快照读)的幻读。

https://zhuanlan.zhihu.com/p/364331468

@kebukeYi
Copy link

嗯嗯 最近又看了很多资料 明白了; 在不同的隔离级别下 加的锁也是 不同的, 间隙锁其实只有在RR下才有, 然后 MVCC 加锁两种方式都是解决 读写 写写 写读的 并发控制方式. [https://huatu.98youxi.com/swdt/loading.htm#c49db3553f8f631a9eb407060b45c4fb]

@hzh-test
Copy link

hzh-test commented Mar 21, 2022

就让我来终结这一切吧!

可以先看某位大神的文章 https://opensource.actionsky.com/20210818-mysql,理解后就不用再往下看了,不理解的话再往下看恐怕也不能理解,哈哈(以下内容没有被执行到)。

1、“幻读“是 ANSI SQL 标准提出的概念,在“可重复读”的隔离级别下依旧会有“幻读”问题。

2、“可重复读”并非字面意义上的“可重复读”,不然就不会有“幻读”问题了,所以这是 ANSI SQL 标准 取名的问题。严谨一点来说不应该叫“可重复读”,叫“可重复读取已读取过的“可能更合适些,或者严格按“可重复读”字面意思解释,这时就不应该会有“幻读“问题。“幻读”概念问题同理,即 此 幻读 非 彼 幻读。

3、所以这里大家要记住一个真理,没有什么是绝对正确的,标准也会有错误或者不严谨的地方。
对于数据库并发一致性问题的描述,存在太多种可能情况,像平时常说的并发一致性问题有:“丢失修改”、“脏读”、“不可重复读”、“幻读”。其实还有其它的,之前看到过一个,忘记叫啥名字了,至于上面四个是不是 ANSI SQL 标准提出的,我不知道,有兴趣可以去官网找找。

4、MySQL 并非严格遵循 ANSI SQL 标准 实现,其它数据库厂商也是,毕竟理论和现实肯定有差别。像 浏览器 就有各种兼容性问题,数据库也一样有。一样的 SQL 语句你不能保证在不同数据库都能正常执行,而且在同一数据库的不同版本下也可能会有不同的执行结果。

5、MVCC 可实现 ANSI SQL 标准 的 “可重复读”,并且是不会有 ANSI SQL 标准 的 “幻读” 问题的,因为读取的都是快照版本。

6、MySQL InnoDB 的 “可重复读” 就是用 MVCC 实现的,所以 InnoDB 是解决了 ANSI SQL 标准 的 “幻读” 问题的。

7、网上大量讨论的 InnoDB 幻读问题,看起来确实是字面意义上的幻读,但是大家要注意这不是 ANSI SQL 标准 的 “幻读”,即 此 幻读 非 彼 幻读。

8、既然 MySQL 并非严格遵循 ANSI SQL 设计,那我们也不用严格按照 ANSI SQL 去思考问题。
在生产中,我们可以这么理解,InnoDB 的 select 是快照读,是不会加锁的,而 insert 、delete 和 update 操作的都是最新版本,是会自动加锁的,且这个锁有可能是 Record Lock / Gap Lock / Next-Key Lock。至于 select for update / select for lock in share mode 则是当前读,也就是数据的最新版本,其效果和 insert、delete 和 update 是一样的,会自动加 Record Lock / Gap Lock / Next-Key Lock,只不过不修改数据而已。
对于 InnoDB 幻读,管它呢!幻不幻读都是上面的结果,不管你叫啥名字,按上面的去思考问题就没错,管它是不是幻读。

9、对于 InnoDB 什么时候用 Record Lock 什么时候用 Next-Key Lock,这个其实很好理解。首先 InnoDB 的锁是加上在索引上的,(个人理解是索引本身有加锁标识),而且 InnoDB 的聚簇索引和数据本来就是在同一个地方,所以在这点上锁加在索引还是数据上其实没大区别,但是二级索引是分开的,所以让我设计的话肯定是把锁加在索引上更容易实现。既然锁是加在索引上的,那要是这个索引不存在呢?像更新某一个范围后,和这个范围内的数据对应的索引就都被上了锁(Record Lock),此时如果另一个事务在这个范围里再新增数据,这个事务是不会被干扰的,因为这里只是创建了新的索引,并只在这个索引上加锁(Record Lock)。所以光靠 Record Lock 是不够的,此时 Gap Lock / Next-Key Lock 就发挥作用了,它会把这个范围都给锁住,这样新增的数据就加不进来。但前面说了,索引只能加在已经存在的索引上,这个锁住一个范围该作何解释?我也没研究过源码,但让我来实现我还是可以的,只要再索引上再加一个标识,用来判断它是不是还有范围锁,并且这个范围还有方向性:向左或向右。这样当新增数据时,只要判断在要插入的索引的两边的索引是否存在范围锁标识,并且左边的范围锁方向向右,右边的范围锁方向向左,那就不给进,这就实现了范围锁。

再提示一遍:更多内容可以查看这篇文章 https://opensource.actionsky.com/20210818-mysql,我也是在看了这篇文章后才终于明白。

@AeneasWu
Copy link

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.
原文

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级别确实防不住幻读

事务a中update后的select变为了当前读, 而上一次select是快照读. 这两次select其实已经不属于一个事务了, 所以前后两次查询结果不一致, 但不属于不可重复读的范畴.

@mwh-mawenhao
Copy link

就让我来终结这一切吧!

可以先看某位大神的文章 https://opensource.actionsky.com/20210818-mysql,理解后就不用再往下看了,不理解的话再往下看恐怕也不能理解,哈哈(以下内容没有被执行到)。

1、“幻读“是 ANSI SQL 标准提出的概念,在“可重复读”的隔离级别下依旧会有“幻读”问题。

2、“可重复读”并非字面意义上的“可重复读”,不然就不会有“幻读”问题了,所以这是 ANSI SQL 标准 取名的问题。严谨一点来说不应该叫“可重复读”,叫“可重复读取已读取过的“可能更合适些,或者严格按“可重复读”字面意思解释,这时就不应该会有“幻读“问题。“幻读”概念问题同理,即 此 幻读 非 彼 幻读。

3、所以这里大家要记住一个真理,没有什么是绝对正确的,标准也会有错误或者不严谨的地方。 对于数据库并发一致性问题的描述,存在太多种可能情况,像平时常说的并发一致性问题有:“丢失修改”、“脏读”、“不可重复读”、“幻读”。其实还有其它的,之前看到过一个,忘记叫啥名字了,至于上面四个是不是 ANSI SQL 标准提出的,我不知道,有兴趣可以去官网找找。

4、MySQL 并非严格遵循 ANSI SQL 标准 实现,其它数据库厂商也是,毕竟理论和现实肯定有差别。像 浏览器 就有各种兼容性问题,数据库也一样有。一样的 SQL 语句你不能保证在不同数据库都能正常执行,而且在同一数据库的不同版本下也可能会有不同的执行结果。

5、MVCC 可实现 ANSI SQL 标准 的 “可重复读”,并且是不会有 ANSI SQL 标准 的 “幻读” 问题的,因为读取的都是快照版本。

6、MySQL InnoDB 的 “可重复读” 就是用 MVCC 实现的,所以 InnoDB 是解决了 ANSI SQL 标准 的 “幻读” 问题的。

7、网上大量讨论的 InnoDB 幻读问题,看起来确实是字面意义上的幻读,但是大家要注意这不是 ANSI SQL 标准 的 “幻读”,即 此 幻读 非 彼 幻读。

8、既然 MySQL 并非严格遵循 ANSI SQL 设计,那我们也不用严格按照 ANSI SQL 去思考问题。 在生产中,我们可以这么理解,InnoDB 的 select 是快照读,是不会加锁的,而 insert 、delete 和 update 操作的都是最新版本,是会自动加锁的,且这个锁有可能是 Record Lock / Gap Lock / Next-Key Lock。至于 select for update / select for lock in share mode 则是当前读,也就是数据的最新版本,其效果和 insert、delete 和 update 是一样的,会自动加 Record Lock / Gap Lock / Next-Key Lock,只不过不修改数据而已。 对于 InnoDB 幻读,管它呢!幻不幻读都是上面的结果,不管你叫啥名字,按上面的去思考问题就没错,管它是不是幻读。

9、对于 InnoDB 什么时候用 Record Lock 什么时候用 Next-Key Lock,这个其实很好理解。首先 InnoDB 的锁是加上在索引上的,(个人理解是索引本身有加锁标识),而且 InnoDB 的聚簇索引和数据本来就是在同一个地方,所以在这点上锁加在索引还是数据上其实没大区别,但是二级索引是分开的,所以让我设计的话肯定是把锁加在索引上更容易实现。既然锁是加在索引上的,那要是这个索引不存在呢?像更新某一个范围后,和这个范围内的数据对应的索引就都被上了锁(Record Lock),此时如果另一个事务在这个范围里再新增数据,这个事务是不会被干扰的,因为这里只是创建了新的索引,并只在这个索引上加锁(Record Lock)。所以光靠 Record Lock 是不够的,此时 Gap Lock / Next-Key Lock 就发挥作用了,它会把这个范围都给锁住,这样新增的数据就加不进来。但前面说了,索引只能加在已经存在的索引上,这个锁住一个范围该作何解释?我也没研究过源码,但让我来实现我还是可以的,只要再索引上再加一个标识,用来判断它是不是还有范围锁,并且这个范围还有方向性:向左或向右。这样当新增数据时,只要判断在要插入的索引的两边的索引是否存在范围锁标识,并且左边的范围锁方向向右,右边的范围锁方向向左,那就不给进,这就实现了范围锁。

再提示一遍:更多内容可以查看这篇文章 https://opensource.actionsky.com/20210818-mysql,我也是在看了这篇文章后才终于明白。

感觉大佬们纠结的点就在于“快照读和当前读混用引起的异常算不算幻读?”
而我也觉得,快照读和当前读混用引起的异常算中文语境下的幻读。
唉,太纠结了,算了,撤了。

@naheux
Copy link

naheux commented Jun 20, 2023

我是从一个知乎帖子找过来的,看了从知乎到本贴的整个讨论过程,学到了很多新东西,同时感觉很有趣。因为分歧出现的原因并不是严格意义上对于一个「客观的事实」存在不同的看法,而是因为语言在日常使用中的“模糊性”产生的「概念上的分歧」,以及由此导致的「比较前提上的分歧」,自此,参与者之间的认知差距越来越远,也让读者看得迷迷糊糊。为了记录我的思考过程,同时帮助后来者梳理讨论内容,尝试写下这篇啰嗦的回复。

什么是「客观的事实」?

相信大家都学过《两小儿辩日》,太阳的实际大小就是我所谓的「客观的事实」,而两小儿因为参照系不同,产生的对太阳大小的不同认知,即为

对于一个「客观的事实」存在不同的看法

什么是「比较前提上的分歧」?

大家应该都听说过「控制变量」这个实验原则。为啥需要「控制变量」,因为需要保证「比较前提」是一致的。一个简单的例子,“水在特定大气压下的沸点是100度”,要达成这个共识,最简单而又最容易忽视的一个「控制变量」,就是“度”的单位是“摄氏度”,而不是“华氏度”。因此要保证讨论过程是层层递进的,就要不断注意我们是否持续共享同一个「比较前提」,否则任何后续讨论就失去了基础,鸡同鸭讲。

而整个讨论过程中貌似存在几个「比较前提上的分歧」。

例如,我们是在讨论有关「幻读」的不同观点,那「幻读」的定义就是描述观点前的「比较前提」,我先说说我的理解:

「幻读」通俗来讲,是指在一次事务中【两次相同】的select查询,后一次读到的记录【多于】前一次读到的记录。

注意必须是【多于】,不能是【少于】或者【内容有异】,后者是「不可重复读」的定义。因此描述观点时,起码要先保证列举的现象确实是「幻读」而不是「不可重复读」。

将「幻读」与「不可重复读」混淆的情况,是个容易说明的分歧,甚至不能严格称为“分歧”,而是“误解”。因为误解并非观点不同,而是认知有误。但是,另一个分歧就相对难以解释清楚了,那就是幻读定义中的「两次相同的select查询」。

什么是「两次相同的select查询」?

对于「MySQL在Repeatable-Read级别下是否解决幻读」这个问题上的主要分歧,看起来是对于【两次相同】这个前提存在不同的看法。

认为没有解决幻读的主要例证如下:

事务 A 事务 B
begin
select * from t1 where a > 1(不需要加锁,由MVCC发挥作用读到的是快照)
begin
insert into t1 (a) values(2)
commit
select * from t1 where a > 1 for update(需要加锁,此时会读到新增的数据,因此认为存在幻读)
commit

所以,现在问题变成了,带for update的语句和不带for update的语句,是【两次相同】吗?

观点1:仅从SQL查询范围考虑,是【两次相同】
观点2:不能仅从SQL查询范围考虑,还要考虑for update,因此是【不同的】

不能忽视for update的原因

因为for updatelock in share mode直接影响了select语句的可见范围要求,而可见范围要求来自查询请求本身,与底层实现方式无关,分析如下:

1.不加修饰的select查询,隐含了可见范围对并发事务【不感知】的要求。也即只要我自己不改,我读到的数据就一定是不变的,不受其他事务的影响,无论读一次还是读两次。而【只要我自己不改】也就是前面其他朋友提到的事务READ-ONLY的含义。
2.for updatelock in share mode修饰的select,隐含了可见范围对并发事务【感知】的要求。也即,即使在事务期间,也要读到我关心这些数据的最新状态。

因此,例子中的两个查询语句不能认为是【两次相同】的读,因为它们对可见范围的要求存在本质上的冲突。并不是底层实现方式导致select for update感知到了并发事务的更新,而是这个语句自身要求感知更新。换句话说,这两次查询的结果不同,是符合这两次查询本身的意图的,是预期内的,因此这个例子并非「幻读」想描述的场景。而「幻读」想描述的,是预期之外的异常。

「幻读」想描述什么

看待「MySQL在Repeatable-Read级别解决了幻读」这个观点,一个合适的角度,可能是去搞清楚「幻读」存在的场景是什么样的。从前面的分析中可以看出,之所以要讨论【两次相同】这个「比较前提」的含义,是因为其含义帮助描述了可能产生预期外异常的场景。尝试描述如下:在场景中,不存在并发事务时,事务A的行为本身预期不存在异常,是并发事务的存在导致了异常产生的可能,对于事务A来说,是预期外的异常。

基于各位的讨论,在MySQL的语法中,可能存在「幻读」的场景应该有以下两种(为了简洁,下面的描述省略了两次查询范围一致这一点):

场景1:两次不加修饰的select查询
两次不加修饰的select查询,其语义对可见范围的要求是一致的,都是【不感知】其他事务的修改。因此在事务是READ-ONLY时,第二次查询可以预期,和第一次查询时的数据快照保持一致(这也是MVCC的解决思路,使用快照控制可见范围)。事务READ-ONLY,要求除了我们关注的两次读查询,不能存在其他写操作或锁读操作,否则事务对可见范围的要求就会改变,对查询结果的预期就会改变。

场景2:两次for updatelock in share mode修饰的select(或称锁读操作)
使用这两种修饰的两个select对可见范围的要求也是一致的,和其他写操作相同,即使是在事务中,也要【感知最新数据】。同时,由于第一次select语义要求在查询之后,查询匹配范围内不允许其他事务写操作,因此,第二次select可以预期查询结果跟第一次一致(同时也是最新的)。实现上,行锁解决了存在行的写操作互斥,间隙锁则补上了不存在行(也即间隙)的写操作互斥。

那么,现在我们可以说,MySQL的Repeatable-Read级别解决了上述两种场景下的「幻读」,在上述场景中两次查询结果如果不一致,才是【预期外的】。
总结一下,MySQL InnoDB引擎在Repeatable-Read级别中:

1.在READ-ONLY事务的可见范围要求下,通过多版本并发控制MVCC(Multi-Version Concurrency Control)避免了「幻读」问题;
2.在锁读操作的可见范围要求下,通过引入间隙锁避免了「幻读」问题;
3.两次读操作中间,事务的可见范围如果发生变化(如存在写操作/第二次读是锁读),由此产生的第二次查询结果不同,并非「幻读」,因为此时可见范围的修改是事务主动调整的,也即主动修改了可见数据的预期,也就不再(也不能)要求第二次查询结果不变,也就不存在预期外的结果不同。

我很赞同前面某位朋友的观点,讨论这个问题的最大价值,应该是为了学习MySQL在解决并发事务问题中的关键技术,以及「幻读」这种现象在实践中存在的原因和应对方式,而不是争论MySQL到底有没有解决「幻读」这个观点本身,因为这可能已经偏离了讨论的初衷。

题外话:尝试翻译题主引用的英文资料,以及一些个人理解

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.

翻译:“对加锁的读操作(FOR UPDATELOCK IN SHARE MODE修饰的SELECT)、UPDATEDELETE语句,加锁的细节取决于查询条件是否使用了唯一索引及其唯一匹配条件,或范围查询条件。对于查询条件是唯一索引的唯一匹配这种情况(译者注:where unique_column = xxx),InnoDB只会对匹配的行本身加锁,而不对其前面的间隙加锁(译者注:因为唯一索引的性质已经保证了不会有第二条匹配的记录写入)。而其他查询条件,InnoDB则会使用间隙锁或next-key锁,锁住搜索过程涉及的索引范围(译者注:理论上是符合查询条件的索引范围),以此阻塞其他session向该索引范围的插入操作。”

“而其他查询条件”这种情况,个人理解,除了唯一索引的范围查询,应该也适用于非唯一索引的范围查询(where any_column > xxx)。另外还有文中没有明说,但可以推测出来的,非唯一索引的精确匹配(where any_column = xxxany_column in (xxx, yyy))是锁行本身和其前面的间隙(locks the index record found, and the gap before it.)。在索引树中,如果存在两条any_column = xxx的记录,搜索any_column = xxx的过程就是从根节点开始,一路找到第一个any_column = xxx的记录,然后再继续搜索其左侧范围,直到第一个小于xxx的记录(假设为zzz),而第二个any_column = xxx的记录必然在这个范围中,以此类推,第三个第四个都是如此,向这个范围中的并发insert可能产生「幻读」。所以这时被锁的间隙范围是(zzz, xxx),也即the gap before it。

参考资料:

  1. 前面大家的讨论
  2. https://zhuanlan.zhihu.com/p/117476959
  3. https://opensource.actionsky.com/20210818-mysql/

------------------------------------------------------内容补充------------------------------------------------------

补充一个MySQL官方手册中的描述

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
By default, InnoDB operates in [REPEATABLE READ] transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 15.7.4, “Phantom Rows”).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests