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

幻读:听说被MVCC干掉了?_0

时间:2023-03-18 11:27:14 科技观察

【.com原稿】我是幻读。听说有人认为我是用MVCC解决的。为了让大家更全面的了解我,我只能自己解释一下。图片来自Pexels我是谁?首先让我给你做一个简短的自我介绍。我是事务并发时会出现的三大问题之一。我另外两个兄弟的脏读和不可重复读在上一轮被MVCC无情的干掉了。至于上一轮发生了什么,大家可以去看剧情回顾。我的原因是当master在操作一组数据的时候,很多人也在操作这组数据。举个简单的例子:一组数据按条件过滤后返回100结果,但是其他人在master运行时添加了满足条件的新数据,master再次查询时返回101。第二次返回的数据与第一次返回的数据不一致。于是我诞生了,大家还给我起了个好听的名字,欢都。为什么我会得到这个名字!那是因为我给人的现象好像是幻觉。为什么有人觉得我被MVCC干掉了为了演示方便,我直接用之前的测试表来操作。同时可以看到这张表还有一些测试数据,一切从头开始,清空表。清表命令:truncatetable_name执行该命令会清空表中的数据,自增ID将从1开始。从执行过程来看,truncatetable类似于droptable,然后create桌子。这里的环境是测试环境。线上不要操作,绕过DML方式,无法回滚。小插曲过后,进入正题。根据上图的执行步骤,预计左边事务的第一条select语句的查询结果为空。第二次select查询的结果是1条数据,包括右边事务提交的数据。但是在实际测试中,第一次执行select和第二次执行select返回的结果是一样的。从这个案例可以得出结论,幻读问题在不可重复隔离级别下(快照读的前提下)确实会得到解决。我真的被MVCC解决了吗?从上面的测试用例来看,我的问题似乎可以通过MySQL中的MVCC来解决。既然我的问题都解决了,为什么会有序列化的隔离级别呢?!如此迷茫!继续试验这个问题。为了方便,不再使用上面的表结构,建立一个简单的表结构。进入另一集。你知道如何在MySQL终端中清屏吗?执行命令systemclear。然后开始新一轮的测试:在上图中事务1的案例中,有几次查询数据为空。此时,事务2已经成功插入并提交了数据。但是当事务1多次查询数据后插入数据为空时,提示主键重复。再来看另一种情况:如上图所示:step1:事务1开启一个事务step2:事务2开启一个事务step3:事务1查询数据只有一条数据step4:事务2添加一条数据step5:事务1查询数据作为一个step6:事务2提交一个事务step7:事务1查询数据是一条数据step8:事务1修改名称step9:猜猜此时表中的数据会发生什么情况。这种情况下,事务1一直读取一条数据,但是修改数据时影响的数据行数却是2条,再次查看数据时,出现了事务2添加的数据。这也可以看作是幻读。总结:通过以上两个案例,我们知道MySQL的可重复读隔离级别并没有完全解决幻读问题,只是解决了快照读下的幻读问题。但是目前的读操作仍然存在幻读的问题,也就是说MVCC对幻读的解决并不彻底。我们来谈谈当前阅读和快照阅读。上一轮已经消化了快照阅读和当前阅读。为了防止消化不良,这里简单说明一下。①当前所有的读操作都是加锁的,除共享锁外,其他锁都是互斥的。如果要增删改查,需要等待锁释放,所以读到的数据是最新的记录。简单的说就是当前读加锁,增删改查,不管锁是共享锁还是排它锁,都是当前读。在MySQL的Innodb存储引擎下,增删改查操作默认会被锁定,所以增删改查操作默认为当前读取。②快照读快照读的出现旨在提高事务并发性,是基于我的敌人MVCC。简单的说,snapshotread是一个没有加锁的非阻塞读,也就是一个简单的select操作(select*fromuser)。在Innodb存储引擎下执行简单的select操作时,会记录当前快照读取的数据,后续的select会跟随从第一个快照读取的数据,即使有其他事务提交,也不会影响当前的select结果,这是解决了不可重复读的问题。快照读取的数据虽然是一致的,但不一定是最新的数据,而是历史数据。让我告诉你!现在的阅读情况,我被next-key锁死了。在第二节中了解到我在snapshotreading下引起的问题已经被MVCC排除了。但是在第三节的案例测试中,我发现我在当前阅读中满血复活了。如果我这么容易被杀死,我怎么能被称为无敌小强呢?这不是一个玩笑!毕竟,如果MVCC带上它的小弟next-key锁,那我就完蛋了,再也不会像灰太狼说的那句经典名言“我一定会回来的”。此时,我们需要思考一个问题。Innodb存储引擎下,快照读取默认加next-key锁,还是需要手动加锁。官方文档对next-keylocks的解释:为了防止幻象,InnoDB使用了一种叫做next-keylocking的算法,结合了index-rowlocking和gaplocking。InnoDB执行行级锁定的方式是,当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享或排他锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的下一个键锁也会影响该索引记录之前的“间隙”。也就是说,下一个键锁是索引记录锁加上索引记录之前的间隙上的间隙锁。如果一个会话在索引中的记录R上有共享锁或排他锁,则另一个会话不能在索引顺序中紧接在R之前的间隙中插入新的索引记录。粗略地说,Innodb为了防止幻读,采用了next-key锁算法,将行锁(recordlock)和间隙锁(gaplock)结合起来。Innodb行锁搜索或扫描表索引时,会对遇到的索引记录设置共享锁或排他锁,所以行锁实际上是索引记录锁。另外,在索引记录上设置的锁也会影响索引记录之前的“间隙”。也就是说,下一个键锁是索引记录行加上索引记录之前的“间隙”的间隙锁。并且还给出了一个案例:SELECT*FROMchildWHEREid>100FORUPDATE;当Innodb扫描索引时,它会锁定大于100的id,防止任何大于100的数据被添加。至此,上面的问题已经有了答案。解决Innodb下当前读导致的幻读问题,需要手动加锁来解决。我们再看一个案例。下图是此时的数据情况:下图中的案例解决了第三节第一个案例的幻读问题。如上图:步骤事务1:启动事务步骤事务2:启动事务步骤事务1:查询ID为4的数据并加排它锁步骤事务2:添加ID为4的数据,等待事务1释放锁步骤事务1:添加ID为4的数据,添加成功步骤事务1:查询当前数据步骤事务1:提交事务步骤事务2:报错,返回主键重复问题本例中索引查询的列是主键并且是唯一的。这时Innodb引擎会降级next-key锁,即只锁当前查询的索引记录行,不锁范围锁。Case2:还是用上面的数据,但是这次我们进行范围搜索。此时的数据为1、3、5,查找范围大于3。从下图可以看出,在执行事务2时,添加ID2可以添加成功。但是添加ID6时需要等待。此时如果事务1不提交事务,事务2将添加ID为6的数据失败。对于上面的SQL语句select*fromuserwhereid>3forupdate;执行只返回第5行,此时锁定范围为(3,5],(5,∞),所以id为2的可以插入,id为4或大于4的可以插入5无法插入以上就是InnodbSolution中幻读问题的最终解决方案幻读解决方案为了方便大家直观理解幻读解决方案,这里简单总结一下MVCC解决snapshot下幻读问题reading.为什么能解决呢,在第一次执行一个简单的select语句时会生成一个snapshot,后面的select查询会跟随第一次snapshotread的结果。因此,快照读取查询得到的数据可能是历史数据。使用next-keylock解决当前阅读的幻读问题。Next-keylock是recordlock和gaplock的组合。它锁定一个范围。如果查询数据是索引记录行,则只会锁定当前行,即降级为记录锁。如果是范围搜索,则会锁定一个范围。比如上面的例子,如果ID为1,3,5查询大于3的数据,就会锁定范围内的(3,5),(5,∞),其他事务锁定是不能的在发布前插入,从官方文档也可以知道,如果需要校验数据的唯一性,只需要在查询中加共享锁,即在select中加inlocksharemode语句。如果返回结果为空,则可以插入,插入的值必须是唯一的。也可以加nextkeylock,防止别人同时插入相同的数据。第5节的所有情况都是next-keylockused从这一点我们可以知道next-keylock可以锁住表中不存在的索引,根据上面的结论,如果要使用共享锁来检测数据的唯一性,那么如果多个事务同时开共享锁,同时添加相同的数据?会不会有问题?明明是st它不会。如果多个事务同时插入相同的数据,只有一个事务会添加成功,其他事务会抛出错误。这是一个新概念“死锁”。ExtendedtransactionID是什么时候分配的?从这篇文章或其他资料中可以得到的一个信息是,执行一个简单的select语句时,也会产生read-view。虽然snapshotreading和read-view是以事务启动为前提的,但是read-veiw是由未提交的事务ID组成的。①那么交易ID是什么时候分配的呢?启动事务有两种方式,一种是显示启动,另一种是设置autocommit=0后执行select,事务才会启动。启动显示最简单的方法是从begin语句开始,也可以使用starttransaction来启动事务。如果使用starttrancaction来启动事务,还可以选择启动只读事务或读写事务。看了很多资料,据说在启动一个事务的时候,会分配一个事务ID,那么我们来验证一下是不是长这个样子的?从上图中可以看出,执行begin语句时,查询事务ID为空,也就是说执行begin后trx_id没有分配。然后执行begin的时候,支持DML语句!根据文档,begin命令的执行并没有真正启动一个事务,只是为当前线程设置了一个标记,表示它是一个显式开启的事务。所以需要明白,对数据进行增删改查等操作后,才真正开始了一个事务。这个时候事务会在引擎层启动。②为什么交易ID相差很大?上图中查询的是当前活跃的交易ID,但是这两个交易ID相差特别大。相信很多朋友都遇到过这个问题。如果你有问题,你不害怕。你怕的是没有问题。其实这两条数据中只有20841才是真正的交易ID,那么第二条数据中的ID是什么呢!知道这个数字是什么的前提是知道它是怎么来的。从上图可以看出,在执行select语句的时候,会产生一个非常大的事务ID。是否可以理解为这个差异很大的交易ID是通过快照读取产生的?然后在这个事务下执行一条insert语句,然后查看事务ID的状态。令人难以置信的是,在事务中先执行select语句,然后再执行insert语句。交易ID已更改。是什么原因?查询资料得知,执行一个简单的select语句时,称为只读事务。为了避免给只读事务分配trx_id带来不必要的开销,没有给它分配事务ID。只读事务不分配撤销段,也不会分配LOCK锁结构。本质上,只读事务的trx_id的值为0。但是为了执行select*frominformation_schema.INNODB_TRX或者showengineinnodbstatus。将通过reinterpret_cast(trx)|(max_trx_id+1)将指针转换为一个64字节的非负整数然后位或(max_trx_id+1)就是这样一个值。这个值的生成过程就不用深究了。你只需要知道只读事务下不会分配事务ID,查询的值只是为了显示,没有实际意义。但是执行select*frominformation_schema.INNODB_TRX查询事务ID时,通过showengineinnodbstatus查询是查不到的。在Innodb下,如果事务是只读事务,在Innodb数据结构中是不会显示的,所以看不到。作者:卡卡介绍:坚持学习、博客、分享是卡卡从业以来一直秉承的信念。希望庞大的网络中的卡卡文章能给大家带来一点帮助。我是卡卡,下次见。编辑:陶佳龙征稿:如有意投稿或求报道,请加编辑微信gordonlonglong【原创稿件,合作网站转载请注明原作者及出处为.com】