别再误解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.

上面这句话摘自MySQL的官方手册。它只说明了读会读到上一次没有返回的记录,看起来是幻影一般。如果你理解到这里,那么恭喜你,你会遇到各种困惑。 其实幻读的现象远不止于此,更不仅仅只是两次「读」,第二次「读」来发现有幻觉。

MySQL的InnoDb存储引擎默认的隔离级别是REPEATABLE-READ,即可重复读。 那什么是「可重复读」呢,简单来说就是一个事务里的两个相同条件的查询查到的结果应该是一致的,即结果是「可以重复读到的」,所以就解决了「幻读」。

OK,听起来很简单,一个隔离级别就可以搞定了,但是内部的机制和原理并不简单,并且有些概念的作用可能大家并不知道具体解决了什么问题。

MVCC(Multi-Version Concurrency Control多版本并发控制):

MVCC每次更新操作都会复制一条新的记录,新纪录的创建时间为当前事务id

优势为读不加锁,读写不冲突

InnoDb存储引擎中,每行数据包含了一些隐藏字段 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT

DATA_TRX_ID 字段记录了数据的创建和删除时间,这个时间指的是对数据进行操作的事务的id

DATA_ROLL_PTR 指向当前数据的undo log记录,回滚数据就是通过这个指针

DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在mysql进行数据的GC,清理历史版本数据的时候。

具体的DML:

INSERT:创建一条新数据,DB_TRX_ID中的创建时间为当前事务id,DB_ROLL_PT为NULL

UPDATE:将当前行的DB_TRX_ID中的删除时间设置为当前事务id,DELETE BIT设置为1

DELETE:复制了一行,新行的DB_TRX_ID中的创建时间为当前事务id,删除时间为空,DB_ROLL_PT指向了上一个版本的记录,事务提交后DB_ROLL_PT置为NULL

可知,为了提高并发度,InnoDb提供了这个「非锁定读」,即不需要等待访问行上的锁释放,读取行的一个快照即可。 既然是多版本读,那么肯定读不到隔壁事务的新插入数据了,所以解决了幻读。

Read Uncommitted每次都读取记录的最新版本,会出现脏读,未实现MVCC

Serializable对所有读操作都加锁,读写发生冲突,不会使用MVCC

(RR级别)InnoDb检查每行数据,确保它们符合两个标准:

只查找创建时间早于当前事务id的记录,这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

行的DELETE BIT为1时,查找删除时间晚于当前事务id的记录,确定了当前事务开始之前,行没有被删除

(RC级别)每次重新计算read view,read view的范围为InnoDb中最大的事务id,为避免脏读读取的是DB_ROLL_PT指向的记录

就这么简单吗? 其实幻读有很多种出现形式,简单的SELECT不加条件的查询在RR下肯定是读不到隔壁事务提交的数据的。但是仍然可能在执行INSERT/UPDATE时遇到幻读现象。因为SELECT 不加锁的快照读行为是无法限制其他事务对新增重合范围的数据的插入的。

所以还要引入第二个机制。

其实更多的幻读现象是通过写操作来发现的,如SELECT了3条数据,UPDATE的时候可能返回了4个成功结果,或者INSERT某条不在的数据时忽然报错说唯一索引冲突等。

首先来了解一下InnoDb的锁机制,InnoDB有三种行锁:

Record Lock:单个行记录上的锁

Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况

Next-Key Lock:前两个锁的加和,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题

如果是带排他锁操作(除了INSERT/UPDATE/DELETE这种,还包括SELECT FOR UPDATE/LOCK IN SHARE MODE等),它们默认都在操作的记录上加了Next-Key Lock。只有使用了这里的操作后才会在相应的记录周围和记录本身加锁,即Record Lock + Gap Lock,所以会导致有冲突操作的事务阻塞进而超时失败。

隔离级别越高并发度越差,性能越差,虽然MySQL默认的是RR,但是如果业务不需要严格的没有幻读现象,是可以降低为RC的或修改配置innodb_locks_unsafe_for_binlog为1 来避免Gap Lock的。 注意有的时候MySQL会自动对Next-Key Lock进行优化,退化为只加Record Lock,不加Gap Lock,如相关条件字段为主键时直接加Record Lock。

凡是在REPEATABLE-READ中执行的语句均不会遇到幻读现象。

这个显然是错误的。REPEATABLE-READ只是有机制可以用来防止幻读的发生,但如果你没有「使用」或「激活」它相关机制,你仍然会遇到幻读现象。

REPEATABLE-READ肯定不会读到隔壁事务已经提交的数据,即使某个数据已经由隔壁事务提交,当前事务插入不会报错,否则就是发生了幻读。

简单来说前半句话是对的,后半句有什么问题呢?可REPEATABLE-READ中如何「读」是我们自己来写SELECT 的,如果不加锁则属于快照读,当前事务读不到的数据并不一定是不存在的,如果已经存在对应的数据,那么当前事务尝试插入的时候是可能会失败的。 而插入失败的原因可能是因为主键冲突导致数据库报异常,跟隔离级别无直接关系。任何隔离级别下插入已经存在的数据都会报错。

一句话,看不到并不代表没有,并不代表可以自以为然的插入无忧。

REPEATABLE-READ的事务里查不到的数据一定是不存在的,所以我可以放心插入,100%成功。

这个观点也是错的,查不到只能说明当前事务里读不到,并不代表此时其他事务没有插入这样的数据。 如何保证判断某个数据不存在以后其他事务也不会插入成功?答案是上Next-Key Lock。不上锁是无法阻止其他事务插入的。

SELECT * FROM table1 WHERE id >100

上面这个语句在事务里判断后如果不存在数据是无法保证其他事务插入符合条件的数据的,需要加锁

SELECT * FROM table1 WHERE id >100 FOR UPDATE;

此时如果有隔壁事务尝试插入大于100的id的数据则会等待当前事务释放锁,直到超时后中断当前事务。

(waiting for lock … then timeout) ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

说了这么多,有一点要注意,就是这个Next-Key Lock一定是在REPEATABLE-READ下才有,READ-COMMITTED是不存在的。

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.

即InnoDb在REPEATABLE-READ下提供Next-Key Lock机制,但是需要业务自己去加锁,如果不加锁,只是简单的SELECT查询,是无法限制并行事务的插入的。

凡是REPEATABLE-READ中的读都无法读取最新的数据。

这个观点也是错误的,虽然我们读取的记录都是可重复读取的,但是如果你想读取最新的记录可以用加锁的方式读。

If you want to see the “freshest” state of the database, you should use either the READ COMMITTED isolation level or a locking read:

以下任意一种均:

SELECT * FROM table1 LOCK IN SHARE MODE;

SELECT * FROM table1 FOR UPDATE;

但这里要说明的是这样做跟SERIALIZABLE没有什么区别,即读也加了锁,性能大打折扣。

如果使用了当前读加了锁,但是锁的行并不存在则不会阻止隔壁事务插入符合条件的数据。

其实记录存在与否和事务加锁成功与否无关,如SELECT * FROM user WHERE id = 5 FOR UPDATE,此时id=5的记录不存在,隔壁事务仍然无法插入记录(假设当前自增的主键id已经是4了)。因为锁定的是索引,故记录实体存在与否没关系。

MySQL中的幻读只有在读的时候才会发生,读这里特指SELECT操作。

其实INSERT也是隐式的读取,只不过是在MySQL的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入的。 不可重复读测试「读-读」,而幻读侧重「读-写」,用写来证实读的是幻影。为啥幻读不是侧重「读-读」呢?因为MVCC保证了一个事务是不可能读到另外一个事务的新插入数据的,所以这种场景下不会发生幻读。

转载:http://www.yangchengec.cn/shiyong/566.html

原文地址:https://www.cnblogs.com/yaphetsfang/p/12055250.html

时间: 2024-10-02 05:58:49

别再误解MySQL和「幻读」了的相关文章

mysql repeatable-read 一次利用间隙锁解决幻读案例

repeatable-read是Mysql默认事务隔离级别!能解决脏读以及不可重复读的问题,但可能出现幻读的情况 不可重复读:在一个未提交的事务里,二次查询结果可能不相同,因为在这个事务执行过程中,外面的   事务可能对这个数据集进行了修改并提交! 幻读:一个事务在操作过程中!有别的事务对此数据集进行了修改并提交,但这些操作第一个事务读不到,等到这个事务提交的时候,便有可能引起明明插入的数据没有查询到,但却出现插入重复的错误! 不可重复读与幻读的区别: 不可重复读是能读到其它事务已经提交的数据,

mysql 幻读

幻读(Phantom Read) 是指当用户读取某一范围的数据行时,B事务在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行.InnoDB和Falcon存储引擎通 过多版本并发控制机制解决了幻读问题. http://bbs.csdn.net/topics/360050896 http://narcissusoyf.iteye.com/blog/1637309 http://hudeyong926.iteye.com/blog/1490687 http://imysql.c

总是忘记的场景:MySQL InnoDB四个事务级别 与 脏读、不重复读、幻读

MySQL InnoDB事务隔离级别脏读.可重复读.幻读 MySQL InnoDB事务的隔离级别有四级,默认是“可重复读”(REPEATABLE READ). ·        未提交读(READUNCOMMITTED).另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读). ·        提交读(READCOMMITTED).本事务读取到的是最新的数据(其他事务提交后的).问题是,在同一个事务里,前后两次相同的SELECT会读到不同的结果(不重复读).

MySQL InnoDB四个事务级别 与 脏读、不反复读、幻读

MySQL InnoDB事务隔离级别脏读.可反复读.幻读 希望通过本文.能够加深读者对ySQL InnoDB的四个事务隔离级别.以及脏读.不反复读.幻读的理解. MySQL InnoDB事务的隔离级别有四级,默认是"可反复读"(REPEATABLE READ). ·        未提交读(READUNCOMMITTED). 还有一个事务改动了数据,但尚未提交.而本事务中的SELECT会读到这些未被提交的数据(脏读). ·        提交读(READCOMMITTED).本事务读取

MySQL InnoDB四个事务级别 与 脏读、不重复读、幻读

MySQL InnoDB事务隔离级别脏读.可重复读.幻读 希望通过本文,可以加深读者对ySQL InnoDB的四个事务隔离级别,以及脏读.不重复读.幻读的理解. MySQL InnoDB事务的隔离级别有四级,默认是"可重复读"(REPEATABLE READ). ·        未提交读(READUNCOMMITTED).另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读). ·        提交读(READCOMMITTED).本事务读取到的

MySQL的可重复读级别能解决幻读吗

引言 之前在深入了解数据库理论的时候,了解到事物的不同隔离级别可能存在的问题.为了更好的理解所以在MySQL数据库中测试复现这些问题.关于脏读和不可重复读在相应的隔离级别下都很容易的复现了.但是对于幻读,我发现在可重复读的隔离级别下没有出现,当时想到难道是MySQL对幻读做了什么处理? 测试: 创建一张测试用的表dept: CREATE TABLE `dept` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT

一文带你理解脏读,幻读,不可重复读与mysql的锁,事务隔离机制

首先说一下数据库事务的四大特性 1 ACID 事务的四大特性是ACID(不是"酸"....) (1) A:原子性(Atomicity) 原子性指的是事务要么完全执行,要么完全不执行. (2) C:一致性(Consistency) 事务完成时,数据必须处于一致的状态.若事务执行途中出错,会回滚到之前的事务没有执行前的状态,这样数据就处于一致的状态.若事务出错后没有回滚,部分修改的内容写入到了数据库中,这时数据就是不一致的状态. (3) I:隔离性(Isolation) 同时处理多个事务时

面试官:MySQL的可重复读级别能解决幻读问题吗?

引言 之前在深入了解数据库理论的时候,了解到事务的不同隔离级别可能存在的问题.为了更好的理解所以在MySQL数据库中测试复现这些问题.关于脏读和不可重复读在相应的隔离级别下都很容易的复现了. 但是对于幻读,我发现在可重复读的隔离级别下没有出现,当时想到难道是MySQL对幻读做了什么处理? 测试: 创建一张测试用的表dept: CREATETABLE`dept`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(20)DEFAULTNULL,PRIMAR

mysql事务隔离级别/脏读/不可重复读/幻读详解

一.四种事务隔离级别 1.1 read uncommitted 读未提交 即:事务A可以读取到事务B已修改但未提交的数据. 除非是文章阅读量,每次+1这种无关痛痒的场景,一般业务系统没有人会使用该事务隔离级别,标准实在太宽松了. 1.2 read committed 读已提交(简称RC) 即:事务A只能读取到事务B修改并已提交的数据. 这个级别相对要严格一些,至少是要等其它事务把变更提交到db,才能读取到,听上去蛮靠谱的.但是有些业务场景,比如会员系统中,如果要在一个事务中,多次读取用户身份,判