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

没意思!闺蜜突然问我MVCC的实现原理

时间:2023-03-21 21:03:54 科技观察

[.com原稿]我们都知道事务的可重复读级别的实现原理是使用MVCC实现的,那么你对MVCC的底层实现原理了解多少MVCC!面试更频繁,你值得拥有。图片来自Pexels。什么是MVCC?MVCC是一个多版本控制器。它的特点是不同的事务可以同时读取不同版本的数据,从而解决脏读和不可重复读的问题。这个解释你已经看了几十遍了吧?但是你真的了解什么是多版本控制器吗?需要支付每月的物业费。于是小Q和女友同时登录了小区提供的物业缴费系统。②悲观并发控制假设小Q正在查看当月需要缴纳多少费用,然后进行支付,此时小Q查询到的数据已经被锁定。然后小Q的女朋友就不能访问数据了,直到小Q完成支付或者退出系统解除悲观锁,小Q的女朋友才能查询到数据。悲观锁保证同一时间只能有一个线程访问。默认数据在访问时会发生冲突,然后全程加锁。这样的系统从程序员的角度来看是没有用户体验感的。如果多人需要同时访问一条信息,他们只能在一台设备上查看!③乐观并发控制查看小Q欠物业费,支付的同时,小Q的女朋友也可以访问数据。乐观锁认为即使在并发环境下,也不会有冲突,所以不会做加锁操作。相反,它检测数据何时提交,如果发现冲突则返回冲突信息。总结:Innodb的MVCC机制是乐观锁的一种体现。读不加锁,读写不冲突。没有锁,可以并发读写多个事务,解决读写冲突的问题,大大提高了系统的并发性。悲观锁和乐观锁按粒度分为表锁、行锁、页锁;根据用途分为共享锁和排它锁;按照思路分为乐观锁和悲观锁。不管是乐观锁还是悲观锁,都只是一种想法,并不是实际的锁机制。这必须清楚。①悲观锁(悲观并发控制)悲观锁其实就是悲观并发控制,简称PCC。悲观锁持消极态度,认为每次访问数据时总会发生冲突。因此,每次访问都要先对数据加锁,访问完成后再释放锁。保证同一时间只能有单线程访问,实现数据独占。同时利用数据库本身的锁机制实现悲观锁,可以解决读写冲突。那么在什么场景下可以使用悲观锁呢?悲观锁适用于写多读少的并发环境。虽然并发效率不高,但保证了数据安全。②乐观锁(乐观并发控制)与悲观锁相同。乐观锁其实就是乐观并发控制,简称OCC。与悲观锁相比,乐观锁认为即使在并发环境下,外部对数据的操作也不会造成冲突,所以不会加锁,只是在提交更新时正式与数据发生冲突。没有检测。如果发现冲突,要么重试,要么切换到悲观策略。乐观并发控制是解决数据库并发场景下的写-写冲突,指的是用无锁的方式解决。MVCC解决了什么问题?在并发事务的情况下,会出现以下问题:脏读:读到没有被其他事务提交的数据。不可重复读:当一个事务读取一条数据时,由于另一个事务修改了数据,导致前后读取的数据不一致。幻读:一个事务先读取某个范围内的数据,而另一个事务在这个范围内添加数据,再次读取发现两次得到的结果不一致。Innodb存储引擎中MVCC的实现,主要是为了提高数据库的并发能力,更好的处理读写冲突,同时实现非锁定、非阻塞的并发读写。但是MVCC可以解决脏读和不可重复读。MVCC使用快照读来解决一些幻读问题,但是修改的时候还是使用当前读,所以还是存在幻读问题。幻读问题最终是通过使用间隙锁来解决的。当前读和快照读在了解MVCC如何解决事务并发带来的问题之前,需要了解两个概念,当前读和快照读。①当前读在读操作上加共享锁和排他锁,DML操作加排它锁。这些操作是当前读取。共享锁和独占锁也叫读锁和写锁。共享锁和共享锁并存,但是如果要修改、添加、删除,必须等到共享锁释放后才能操作。因为在Innodb存储引擎中,DML操作隐式加了排他锁。所以当前read读到的记录是最新的记录,在读数据的时候加了一个锁,保证其他事务无法修改当前记录。②快照阅读看到这里,你对默认的隔离级别有了一定的了解!快照读取的前提是隔离级别不是串行级别,串行级别的快照读取会退化为当前读取。快照读的出现旨在提高事务并发性,其实现是基于本文的主角MVCC,即多版本控制器。MVCC可以认为是行锁的一种变体,但它在很多情况下避免了加锁操作。所以快照读取的数据可能不是最新的,而是之前版本的数据。为什么要提快照阅读!因为read-view是snapshot读取产生的,为了防止后面概念模糊,这里说明一下。③如何区分当前读和快照读。simpleselectswithoutlocks属于snapshotreads:selectidnameuserwhereid=1;对应的是当前读,加上共享锁和独占锁select:selectidnamefromuserwhereid=1lockinsharemode;selectidnamefromuserwhereid=1forupdate;MVCC实现的三大要素终于来到了本文的收尾部分。前面的描述都是在铺垫原理。在此之前,你需要知道MVCC只适用于REPEATABLEREAD(可重复读)和READCOMMITTED(读提交)两种隔离级别下。MVCC的实现原理是通过undolog和Readview这两个隐式字段来实现的。①隐式字段Innodb存储引擎中,如果有聚簇索引,每行记录中会隐藏两个字段。如果没有聚簇索引,就会有一个6byte的隐藏主键。这两个隐藏列记录了一条记录的创建时间和删除记录的时间。这里不要理解为记录时间,存储交易ID。两个隐式字段是DB_TRX_ID和DB_ROLL_PTR。如果没有聚簇索引,会有一个DB_ROW_ID的字段:DB_TRX_ID:最后一次创建和修改记录的事务ID。DB_ROLL_PTR:回滚指针,指向这条记录的前一个版本。隐藏字段实际上有一个删除标志隐藏字段,即记录被更新或删除。这里的删除并不是真正的删除,而是把这条记录的deleteflag改为true(这里是一个伏笔,数据库删除是真的删除了吗?)回滚操作的原子性。现在需要知道的另一个功能是实现MVCC多版本控制器。undolog又分为两种:insert时产生的undolog和updatedelete时产生的undolog。Innodb中insert产生的undolog会在事务提交后删除,因为新插入的数据没有历史版本,所以不需要维护undolog。update和delete操作产生的undolog属于一种类型,事务回滚时需要,读取快照时也需要,所以需要维护多个版本的信息。只有当日志不参与快照读取和事务回滚时,相应的日志才会被purge线程统一删除。purge线程会清理undolog的历史版本,以及del标志标记的记录。估计这里还是模糊了undolog在MVCC中的作用。undolog在MVCC中的作用:undolog保存了一条版本链,使用字段DB_ROLL_PTR连接。当数据库执行一条select语句时,会产生一个一致性视图readview。那么这个读视图就是一个数组,由查询时所有未提交的事务ID组成。数组中最小的交易ID为min_id,创建的最大交易ID为max_id。查询数据结果需要与read-view进行比对,得到Snapshot结果。所以undolog在MVCC中的作用就是根据存储的事务ID进行比较,得到快照结果。③undolog的底层实现假设一开始的数据如下图所示:此时执行一条更新的SQL语句:updateusersetname='niuniuwhereid=1',那么undolog的记录就会发生变化,即也就是说,当一条update语句执行时,会将之前的原始数据copy到undolog日志中。同时可以看到最新的一条记录在最后用一条线连接起来,说明DB_ROLL_PTR记录就是undolog中存放的指针地址。最后可能需要通过指针查找历史数据:④read-view在执行sql语句查询时会生成一个一致的视图,即read-view,它是一个数组,由当时所有未提交的事务id组成ofquery,它由已创建的最大事务ID组成。该数组中最小的交易ID称为min_id,最大的交易ID称为max_id。查询数据结果与read-view进行比较,得到快照结果。因此,产生了以下比较规则。这个规则是用当前记录的trx_id来和read-view进行比较。比较规则如下。⑤如果版本链比较规则落在trx_id,如果落在trx_id>max_id,说明这个版本是以后启动的事务产生的,肯定是不可见的。如果min_id<=trx_id<=max_id,有两种情况:如果该行的trx_id在数组中,说明这个版本是由一个还没有提交的事务生成的,不可见,但是当前自己的交易可见。如果该行的trx_id不在数组中,说明提交的事务生成了这个版本,可见。这里还有一个特例,就是对于已经被删除的数据,在之前的undolog中说了update和delete是同一类型的undolog,delete也可以认为是update的特例.当删除一条数据时,会复制版本链上最新的数据,然后将trx_id更改为删除时的trx_id。同时在记录的头部信息中会有一个删除标志,这个标志写为真表示当前记录已经被删除。查询时,根据版本链的规则找到对应的记录。如果delete标志为true,则表示数据已经被删除,数据不会被返回。如果你不明白这里read-view的生成和版本链比较规则,别着急,也不要在这里浪费时间,请继续往下看,我会用一个简单的case和一个复杂的case来复现以上规则给大家吧。MVCC底层原理案例下图是准备好的素材。这里大家应该明白,select返回的结果是niuniu,也就是事务102修改后的结果:在上图中,可以看到有3个事务在进行中。事务ID100和101是其他修改的表,只需要查询事务ID102修改的表。接下来查看select列查询返回的结果是否为事务ID为102的修改结果。此时生成的read-view为[100,101],102,那么现在可以回头看read-view规则,其中事务ID100为min_id,事务ID102为max_id。这个select语句返回的结果一定是niuniu。那么我们就来看看如何在MVCC中查找数据。当前版本链:然后与102的trx_id进行比较,发现102为max_id,再看版本链比较规则中的第三种情况。如果min_id<=trx_id<=max_id,会有两种情况。这个时候,信息就很清楚了。交易ID102不在数组中,说明这个版本是由已经提交的交易生成的,可见!毫无疑问,查询会返回niuniu的值,先通过这个简单的上面的案例让大家对版本链有一个简单的了解,接下来我会用一个比较繁琐的案例再次给大家演示。Case2这个例子需要知道select的第二个查询结果。深黑色字体。也是根据卡卡的记录。当交易ID100被更新两次时,版本链也会发生变化。此时的版本链如下图所示。红色部分为最新数据,蓝色数据为undolog的版本链数据。你对此时产生的read-view有什么疑问,在RR级别,也就是可重复读的隔离级别。在事务下执行查询时,所有读取视图都是从使用的第一个查询语句生成的。那么此时的read-view就是[100,101],102。查看底层查找步骤:当前数据的事务ID为100,按照规则会落在min_id<=trx_id<=max_id的区间内,当前行的事务ID100在read的数组中-view,表示此时的事务还没有提交是不可见的,继续往版本链下找。这时候查到的交易ID还是100,和上面的流程是一致的。通过查找版本链,会发现事务ID是102,102是read-view的max_id,也会落在min_id<=trx_id<=max_id这个区间,但是和之前不同的是,事务102是不在数组中,说明这个版本的事务已经提交所以可见,最后返回的是牛牛案例3。在同一事务中读取的第一个快照。我们再看一个案例。此时事务ID101也更新了两次数据,然后执行查询,看看会返回什么值:熟悉了Case1和Case2之后,现在我们对undolog版本链有了一定的了解和对比规则我们走吧!第三种情况不详细解释。此时的版本链如下:此时的read-view还是[100,101],102。然后首先根据事务101比较版本链,事务101和事务100都会落在min_id<=trx_id<=max_id的范围内,而且都在数组中,所以数据是不可见的。然后在版本链中继续查找,会发现事务102,这是最大的事务ID,不在数组中,所以是可见的。所以最后的返回结果还是牛牛。案例4,大家可以看到案例3的图片,不同的是增加了一条查询语句,那么假设这两条语句的执行时间相同,会不会返回相同的结果呢?case3中查询到的值为niuniu:其实现在的版本链也和case3一致:说一下查找过程:首先这里的read-view发生了变化,此时的read-view为[101],102.取当前交易ID101,与版本链规则进行比较。如果磁盘是min_id<=trx_id<=max_id,并且在数组中,则数据不可见。然后进入版本链,找到下一条数据的交易ID,还是101,和上一条一致。接下来是事务ID100。事务ID100落入trx_id,所以最终返回结果为niuniu2。总结:在同一个事务中查询,会继续使用第一条查询语句产生的read-view(前提是隔离级别是repeatableread)。通过以上四种情况,在查找版本链的过程中可以总结出一个小技巧:根据这个小技巧可以快速知道版本是否可见:如果当前事务ID在绿色部分,表示事务已经提交,数据可见。如果当前交易ID在蓝色部分,则有两种情况。如果当前事务ID在read-view数组中,则未提交的事务不可见,如果不在数组中,则数据可见。如果落在红色部分,就别想了,以后就别想了。小结看完本文,你在面试中最有可能遇到的问题就是谈谈你对MVCC的理解。本文内容由浅入深,从什么是MVCC到MVCC的底层实现,一步步阐述MVCC的实现原理。本文简单总结一下:MVCC解决了脏读、不可重复读、无锁快照读下的幻读问题。不要以为幻读就完全被MVCC解决了。对于当前读和快照读的理解,简单来说,加锁就是当前读,解锁就是快照读。MVCC实现的三要素:两个隐式字段、回滚日志、读取视图。两个隐含字段:DB_TRX_ID:上次创建记录的交易ID,用于修改记录;DB_ROLL_PTR:回滚指针,指向这条记录的前一个版本。undolog在更新数据时会产生版本链,这是read-view获取数据的前提。read-view在SQL执行查询语句时生成,由提交的事务ID数组和创建的最大事务ID组成。版本上链规则见第六节总结作者:卡卡编辑:陶家龙投稿:如有意投稿或求报道,请加编辑微信gordonlonglong【原创稿件请注明原作者及出处】作为.com合作站点转载】

最新推荐
猜你喜欢