别再误解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
intellij安装>thinkphp 技术导航源码UPDATE:将当前⾏的DB_TRX_ID中的删除时间设置为当前事务id,DELETE BIT设置为1
DELETE:复制了⼀⾏,新⾏的DB_TRX_ID中的创建时间为当前事务id,删除时间为空,DB_ROLL_PT指向了上⼀个版本的记录,事务提交后DB_ROLL_PT置为NULLjavascript数据结构与算法
摆动滑块机构简图可知,为了提⾼并发度,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;
mysql怎么读英语但这⾥要说明的是这样做跟SERIALIZABLE没有什么区别,即读也加了锁,性能⼤打折扣。
如果使⽤了当前读加了锁,但是锁的⾏并不存在则不会阻⽌隔壁事务插⼊符合条件的数据。
其实记录存在与否和事务加锁成功与否⽆关,如SELECT * FROM user WHERE id = 5 FOR UPDATE,此时id=5的记录不存在,隔壁事务仍然⽆法插⼊记录(假设当前⾃增的主键id 已经是4了)。因为锁定的是索引,故记录实体存在与否没关系。
MySQL中的幻读只有在读的时候才会发⽣,读这⾥特指SELECT操作。
其实INSERT也是隐式的读取,只不过是在MySQL的机制中读取的,插⼊数据也是要先读取⼀下有没有主键冲突才能决定是否执⾏插⼊的。不可重复读测试「读-读」,⽽幻读侧重「读-写」,⽤写来证实读的是幻影。为啥幻读不是侧重「读-读」呢?因为MVCC保证了⼀个事务是不可能读到另外⼀个事务的新插⼊数据的,所以这种场景下不会发⽣幻读。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论