InnoDB的MVCC实现原理(InnoDB如何实现MVCC以及MVCC的工作机制)

MVCC(多版本并发控制)

使用锁和锁协议来实现相应的隔离级别来进行并发控制,味道虽好但因为锁会造成事务阻塞,导致并发性能会受到一定的影响。而多版本并发控制使得对同一行记录做读写的事务之间不用相互阻塞等待(写写还是要阻塞等待,因为事务对数据进行更新时会加上排他锁),提高了事务的并发能力,可以认为MVCC是一种解决读写阻塞等待的行级锁。

MVCC的一些重要特性
(1)MVCC只支持RC(读取已提交)和RR(可重复读)隔离级别。
(2)MVCC能解决脏读、不可重复读问题,不能解决丢失更新问题和幻读问题。
(3)MVCC是用来解决读写操作之间的阻塞问题。使得在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。

MVCC是多版本并发控制机制,顾名思义支持MVCC的数据库表中每一行数据都可能存在多个版本,对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,通过读写数据时读不同的版本来避免加锁阻塞。不同的存储引擎实现的MVCC多少有些差异,这里主要讨论下Mysql的默认存储引擎InnoDB对MVCC的实现原理。MVCC的实现主要依赖于数据库在每个表中添加的三个隐藏字段以及事务在查询时创建的快照(read view)和数据库的数据版本链(Undo log)。这里先介绍这三个部分的作用,然后再介绍它们是如何联合作战进行非阻塞的实现RC和RR隔离级别。

三个隐藏字段

DB_TRX_ID(6字节): 它是最近一次的更新或者插入或者删除该行数据的事务ID(若是删除,则该行有一个删除位更新为已删除。但并不是真正的进行物理删除,当InnoDB丢弃为删除而编写的更新撤消日志记录时,它才会物理删除相应的行及其索引记录。此删除操作称为清除,速度非常快)
DB_ROLL_PTR(7字节): 回滚指针,指向当前记录行的undo log信息(指向该数据的前一个版本数据)
DB_ROW_ID(6字节): 随着新行插入而单调递增的行ID。当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。

Read View

read view是读视图,其实就相当于一种快照,主要用途是用来做可见性判断,判断当前事务是否有资格访问该行数据(详情下解)。read view有多个变量,这里将关键变量进行描述,为下文做铺垫。

trx_ids: 它里面的trx_ids变量存储了活跃事务列表,也就是Read View开始创建时其他未提交的活跃事务的ID列表。例如事务A在创建read view(快照)时,数据库中事务B和事务C还没提交或者回滚结束事务,此时trx_ids就会将事务B和事务C的事务ID记录下来。
low_limit_id: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
up_limit_id: 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id,虽然该字段名为up_limit,但在trx_ids中的活跃事务号是降序的,所以最后一个为最小活跃事务ID。
creator_trx_id: 当前创建read view的事务的ID。

Undo log

Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以通过回滚指针顺着undo log链找到满足其可见性条件的记录行版本。

在InnoDB里,undo log分为如下两类:
①insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
②update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

Purge线程:上文提到了InnoDB删除一个行记录时,并不是立刻物理删除,而是将该行数据的DB_TRX_ID字段更新为做删除操作的事务ID,并将删除位deleted_bit设置为true(已删除),将其放入update undo log中。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

MVCC更新操作的实现原理

MVCC机制下实现更新还是会用到排他锁,但由于我们读的时候可以通过快照读,读多个版本避免了使用共享锁,因此可以使得读事务不会因为写事务阻塞。MVCC的优越性在于事务需要读行记录的时候不会因为有事务在更新该行记录而阻塞,事务在写行记录时也不会因为有事务在读数据而阻塞。

更新原理: 假设我现在需要修改行记录A,(1)MVCC更新行记录A时会先用排他锁锁住该行记录A;(2)然后将该行记录复制到update undo log中,生成旧版本行记录B;(3)使行记录A的回滚指针指向这条旧版本B,再在行记录A中修改 用户需要修改的字段,并将DB_TRX_ID字段更新为更新这条记录的事务ID,(4)最后提交事务。(用户需要修改的字段指的是业务字段,比如我们要修改name等)
通过回滚指针,形成了一条当前行记录指向历代旧版本行记录的链表,通过这条链表,我们就可以查询该行记录的多个旧版本。
在这里插入图片描述

MVCC查询操作的实现原理

InnoDB中,事务在第一次进行普通的select查询时,会创建一个read view(快照),用于可见性判断,事务只能查询到行记录对于事务来说可见的数据版本。可见性判断是通过行记录的DB_TRX_ID以及read view中的变量比较来判断。

查询过程如下:
(1) 如果 DB_TRX_ID< up_limit_id,则表明这个行记录最近一次更新在当前事务创建快照之前就已经提交了,该记录行的值对当前事务是可见的,当前事务可以访问该行记录,跳到步骤(4)。
(2) 如果DB_TRX_ID>=low_limit_id,则表明这个行记录最近一次更新是在快照创建之后才创建的事务完成的,该记录行的值对当前事务是不可见的,当前事务不可以访问该行记录。因此当前事务只能访问该行记录的更旧的版本数据。通过该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧数据版本的事务号DB_TRX_ID,然后跳到步骤(1)重新开始判断。
(3) 如果up_limit_id<=DB_TRX_ID< low_limit_id,则表明对这个行记录最近一次更新的事务可能是活跃列表中的事务也可能是已经成功提交的事务(事务ID号大的事务可能会比ID号小的事务先进行提交),比如说初始时有5个事务在并发执行,事务ID分别是1001~1005,1004事务完成提交,1001事务进行select的时候创建的快照中活跃事务列表就是1002、1003、1005。因此up_limit_id就是1002, low_limit_id就是1006。对于这种情况,我们需要在活跃事务列表中进行遍历(因为活跃事务列表中的事务ID是有序的,因此用二分查找),确定DB_TRX_ID是否在活跃事务列表中。
(3.1)若不在,说明对这个行记录最近一次更新的事务是在创建快照之前提交的事务,此行记录对当前事务是可见的,也就是说当前事务有资格访问此行记录,跳到步骤(4)。
(3.2)若在,说明对这个行记录最近一次更新的事务是当前活跃事务,在快照创建过程中或者之后完成的数据更新,此行记录对当前事务是不可见的(若可见则会造成脏读、不可重复读等问题)。因此当前事务只能访问该行记录的更旧的版本数据。通过该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧数据版本的事务号DB_TRX_ID,然后跳到步骤(1)重新开始判断。
(4)可以访问,将该行记录的值返回。

当前读和快照读

快照读:使用普通的select 语句进行查询时会生成快照,进行快照读,快照读不会上锁,根据可见性判断,来决定是读取该行记录的最新版本还是旧版本。(只有使用普通的select语句进行查询才会用到快照读,才享受到了MVCC机制的读写非阻塞的优越性)

当前读:使用select … lock in share mode,select … for update,insert,update,delete 语句等语句进行查询或者更新时,会使用相应的锁进行锁定,查询到的肯定数据库中该行记录的最新版本。

MVCC如何实现RC和RR

MVCC对两个隔离级别实现的差异在其产生的read view(快照)的次数不同。

RC:读取已提交隔离级别,避免了脏读,存在不可重复读、幻读问题。MVCC对该级别的实现就是每次进行普通的select查询,都会产生一个新的快照(不同时间,当前活跃的事务不同,行记录最近一次更新的事务ID也可能不同)。就相当于二级锁协议,进行读操作需要加读锁,读完就释放锁,虽然并发性更好且避免了脏读,但会存在不可重复读。

RR:可重复读隔离级别,避免了脏读和不可重复读,存在幻读问题。MVCC对该级别的实现就是在当前事务中只有第一次进行普通的select查询,才会产生快照,此后这个事务一直使用这一个快照进行快照查,相当于三级锁协议,进行读操作需要加读锁,事务结束才释放。避免了不可重复读。但存在幻读,禁止幻读可以通过Next-Key Locks算法的间隙锁和记录锁实现。

借(偷)来一个例子来对上面讲述的MVCC知识进行具体说明
在这里插入图片描述
在这里插入图片描述

总结

1、MVCC的实现主要依赖于数据库在每个表中添加的三个隐藏字段以及事务在查询时创建的快照(read view)和数据库支持多版本数据,数据库中存在数据版本链(Undo log)。
2、InnoDB支持多版本数据,在更新或者删除数据时,并不会立马删除原有行记录,而是将旧版本存入回滚段中的Undo log内,并通过回滚指针形成一个数据链,可以通过这个指针访问链上的历代数据版本,正是这种机制为通过MVCC进行快照读提供了可能。
3、并不是所有的查询都是进行快照读,使用普通的select 语句进行查询时会生成快照,进行快照读;使用select … lock in share mode,select … for update,insert,update,delete 语句等语句进行查询或者更新时还是会使用锁机制,进行锁阻塞。
4、使用MVCC的作用(意义)是非阻塞的解决了事务读写冲突,提高了并发性能。
5、MVCC只支持RC(读取已提交)和RR(可重复读)隔离级别。
6、MVCC能解决脏读、不可重复读问题,不能解决丢失更新问题和幻读问题。
7、InnoDB使用锁机制和MVCC来共同作用,进行并发控制的,虽然MVCC不能解决幻读和丢失更新问题,但通过与锁机制(行级锁的Next-Key Locks算法的使用、排他锁等)一起作用可以达到可串行化隔离级别的效果,禁止了幻读、更新丢失等问题。

参照:
[MySQL官方文档]
MySQL中MVCC的正确打开方式(源码佐证)

发布了25 篇原创文章 · 获赞 62 · 访问量 6503

猜你喜欢

转载自blog.csdn.net/qq_41008202/article/details/105559613
今日推荐