当前位置: 首页 > 科技观察

美团三面:一直问我,MySQL幻读彻底解决了吗?

时间:2023-03-20 16:16:28 科技观察

大家好,我是小林。美团小伙伴一直在追问《MySQL可重复隔离级别彻底解决幻读了吗?》之前提到过,虽然MySQLInnoDB引擎默认的隔离级别是“可重复读”,但是很大程度上避免了幻读现象(没有完全解决),解决方案有两种:对于快照读(普通select语句),幻读通过MVCC方式解决,因为可重复读隔离级别,事务执行过程中看到的数据和事务启动时看到的数据始终保持一致。即使中间有一条数据被其他事务插入,也无法查询到这条数据,所以很好的避免了错觉阅读的问题。对于当前读(select...forupdate等语句),通过next-keylock(recordlock+gaplock)解决幻读,因为select...forupdate语句执行时,next-keylock,如果另一个事务在next-key锁范围内插入一条记录,insert语句就会被阻塞,无法成功插入,这样就很好的避免了幻读的问题。本次我将给出两个实验场景来说明MySQLInnoDB引擎的可重复读隔离级别的幻读问题。好了,走吧!什么是幻读?我们先看看MySQL文档是怎么定义PhantomRead的:所谓幻读问题,就是同一个查询在不同的时间产生不同的行集,在一个事务中发生。例如,如果SELECT执行两次,但第二次返回第一次未返回的行,则该行是“幻像”行。翻译:当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻影问题。例如,如果SELECT执行两次,但第二次返回第一次未返回的行,则该行是“幻像”行。例如,假设一个事务在T1和T2分别执行了如下查询语句,没有执行任何其他语句:SELECT*FROMt_testWHEREid>100;只要T1时刻和T2时刻执行产生的结果集不一样,就会出现幻读问题,例如:T1时刻执行结果是5行记录,而T2时刻执行结果是6行行记录,则出现幻读问题。T1次执行的结果是有5行记录,而T2次执行的结果是有4行记录,这也是幻读的问题。隔离级别当多个事务并发执行时,可能会遇到“脏读、不可重复读、幻读”等现象。这些现象会对交易的一致性产生不同的程序影响。脏读:从其他事务中读取未提交的数据;不可重复读:前后读取的数据不一致;幻读:前后读取的记录条数不一致。这三种现象的严重程度排序如下:SQL标准提出了四种隔离级别来避免这些现象。隔离级别越高,性能效率越低。这四种隔离级别分别是:readuncommitted(读未提交),指的是当一个事务还没有提交时,它所做的更改可以被其他事务看到;readcommitted表示事务提交后,它所做的更改可以被其他事务看到;可重复读(repeatableread),指的是事务执行过程中看到的数据,与事务启动时看到的数据始终保持一致,MySQLInnoDB引擎默认的隔离级别;可序列化(serializable);记录上会加读写锁,当多个事务读写这条记录时,如果发生读写冲突,后访问的事务必须等待前一个事务完成后才能继续执行;对于不同的隔离级别,并发事务可能出现的现象会有所不同。也就是说:在“readuncommitted”隔离级别下,可能会出现脏读、不可重复读、幻读;在“readcommitted”隔离级别下,可能会出现不可重复读和幻读,但它们是不可能的脏读;在“可重复读”隔离级别下,可能会出现幻读,但不可能出现脏读和不可重复读;在“序列化”隔离级别下,脏读、不可重复读、幻读等现象是不可能发生的。因此,要解决脏读现象,需要升级到“readcommit”以上的隔离级别;解决不可重复读的现象,需要升级到“可重复读”的隔离级别。级别升级为“连载”。不同的数据库供应商对SQL标准中规定的四种隔离级别的支持不同。一些数据库只实现其中几个隔离级别。虽然我们讨论的MySQL支持四种隔离级别,但它与SQL标准中规定的各种隔离级别不同。级别隔离级别允许发生的情况存在一些差异。在“可重复读”隔离级别下,MySQL可以很大程度上避免幻读的发生(注意是很大程度上避免,不是完全避免),所以MySQL并没有使用“序列化”隔离级别来避免幻读的发生,因为使用“序列化”隔离级别会影响性能。虽然MySQLInnoDB引擎默认的隔离级别是“可重复读”,但是很大程度上避免了幻读(没有完全解决),解决方法有两种:对于快照读(普通的select语句),对幻读通过MVCC方式,因为在可重复读隔离级别下,事务执行过程中看到的数据和事务开始时看到的数据永远是一致的,即使中间有另外一个事务插入了一条数据,是这条数据不能被查询,这样就很好的避免了幻读的问题。对于当前读(select...forupdate等语句),通过next-keylock(recordlock+gaplock)解决幻读,因为select...forupdate语句执行时,next-keylock,如果另一个事务在next-key锁范围内插入一条记录,insert语句就会被阻塞,无法成功插入,这样就很好的避免了幻读的问题。快照读如何避免幻读?可重复读隔离级别由MVCC(多版本并发控制)实现。实现方式是在事务启动后,执行完第一条查询语句后,会创建一个ReadView。后续查询语句使用这个ReadView通过ThisReadView可以在undolog版本链中找到事务开始的数据,所以事务中每次查询到的数据都是一样的,即使其他事务在其中插入新记录中间,数据无法查询。所以最好避免幻读问题。作为实验,数据库表t_stu如下,其中id为主键。那么在可重复读隔离级别下,两个事务的执行顺序如下:从实验结果可以看出,即使在事务B中间插入了一条记录,之前和之后的两个查询的结果集事务A之后都是一样的,没有所谓的幻读现象。当下阅读如何避免幻读?在MySQL中,除了普通的查询是快照读之外,其他都是当前读,比如update、insert、delete。在执行这些语句之前,会先查询最新版本的数据,然后再进行进一步的操作。这很容易理解。假设要更新一条记录,另一个事务已经删除了这条记录并提交了事务。这样不会造成冲突吗,更新的时候一定要知道最新的数据。另外,select...forupdate查询语句是当前读取的,每次执行都会读取最新的数据。接下来,我们假设select...forupdate的当前读不会被加锁(实际上是加锁),我们在做另外一个实验。此时事务B插入的记录会被事务A的第二条查询语句查询到(因为当前正在读取),这样前后两次查询的结果集就会不同,就会出现幻读.因此,为了解决在“可重复读”隔离级别下使用“当前读”带来的幻读问题,Innodb引擎引入了间隙锁。(另外,read-commit隔离级别没有间隙锁,只有记录锁)假设范围id为(3,5)的表存在间隙锁,那么其他事务无法插入id=4的记录,从而有效防止幻读现象的发生。举个具体的例子,场景如下:事务A执行完当前read语句后,对表中的记录加一个id范围为(2,+∞]的next-key锁(next-keylock是间隙锁+记录锁的组合),那么事务B在执行insert语句的时候,判断插入的位置被事务A加了next-key锁,所以事务B会产生插入意向锁,同时进入等待状态,直到事务A提交事务。这样就避免了事务A因为事务B插入新记录而产生幻读。幻读是不是就彻底解决了?虽然在可重复下很大程度上避免了幻读readisolationlevel,但是还是不能完全解决幻读,我举个repeatablereadisolationlevel下出现幻读的两种场景,第一种场景是还是以这张表为例:事务A执行查询id=5条记录,此时表中没有这条记录,所以查询不到。#TransactionAmysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromt_stuwhereid=5;Emptyset(0.01sec)然后事务B插入一条id=5的记录并提交事务。#事务Bmysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>insertintot_stuvalues(5,'Xiaomei',18);QueryOK,1rowaffected(0.00sec)mysql>commit;QueryOK,0rowsaffected(0.00sec)这时候事务A更新了id=5的记录,是的,事务A没有看到id=5的记录,但是他去更新了这条记录,这个场景真的不合法,而且,和然后再查询id=5的记录,事务A可以看到事务B插入的记录,这种违规场景就出现了幻读。#TransactionAmysql>updatet_stusetname='Xiaolincoding'whereid=5;QueryOK,1rowaffected(0.01sec)Rowsmatched:1Changed:1Warnings:0mysql>select*fromt_stuwhereid=5;+-复制代码---+----------------+------+|编号|姓名|年龄|+----+---------------+-----+|5|小林编码|18|+----+------------+------+1行in设置(0.00秒)幻读时序图如下:可重复下读隔离级别,事务A在第一次执行正常的select语句时会生成一个ReadView,然后事务B插入一条新的id=5的记录并提交。接下来,事务A更新了id=5的记录,此时这条新记录的trx_id隐藏列的值就变成了事务A的事务id,然后事务A使用普通的select语句查询就可以看到这个record当你读到这条记录时,就会出现幻读。由于这种特殊现象的存在,我们认为MySQLInnodb中的MVCC并不能完全避免幻读。第二种出现幻读的场景,除了上述出现幻读的场景外,还有以下出现幻读的场景。时间T1:事务A首先执行“快照读取语句”:select*fromt_testwhereid>100,得到3条记录。时间T2:事务B插入一条id=200的记录并提交;时间T3:事务A执行“当前读语句”select*fromt_testwhereid>100forupdate得到4条记录,此时也发生了幻读现象。为避免这种特殊场景出现幻读现象,尽量在事务启动后立即执行当前读语句如select...forupdate,因为它会给记录加一个next-key锁,从而避免other事务插入一条新记录。总结MySQLInnoDB引擎的可重复读隔离级别(默认隔离级别),根据不同的查询方式提出避免幻读的解决方案:对于快照读(普通select语句),通过MVCC解决幻读。对于当前读(select...forupdate等语句),通过next-keylock(recordlock+gaplock)解决幻读。我举了两个幻读场景的例子。第一个例子:对于快照读,MVCC不能完全避免幻读。因为当事务A更新事务B插入的一条记录时,事务A前后两次查询的记录条目不同,所以产生幻读。第二个例子:对于当前读,如果事务启动后不执行当前读,而是先执行快照读,然后如果在此期间有另一个事务插入记录,那么当事务使用当前读时对于后续的查询,会发现两次查询的记录条目是不一样的,所以就会出现幻读。所以MySQL的可重复读隔离级别并没有完全解决幻读,只是很大程度上避免了幻读的发生。为避免这种特殊场景出现幻读现象,尽量在事务启动后立即执行当前读语句如select...forupdate,因为它会给记录加一个next-key锁,从而避免other事务插入一条新记录。