当前位置: 首页 > 后端技术 > Python

MySQL事务多版本并发控制(MVCC)实现原理

时间:2023-03-25 21:14:53 Python

MySQL事务多版本并发控制(MVCC)实现原理ConcurrencyControl的英文全称是MultiversionConcurrencyControl,简称MVCC。多版本并发控制(MVCC)通过保存某个时间点的数据快照来实现并发控制。也就是说无论事务执行多长时间,在事务内部看到的数据都不会受到其他事务的影响。根据事务的开始时间,每个事务可能会同时看到同一张表的不同数据。的。多版本并发控制的思想是保存数据的历史版本,通过对数据行的多版本管理实现数据库的并发控制。这样我们就可以通过版本号的比较来判断数据是否显示出来,并且在读取数据的时候可以在不加锁的情况下保证事务的隔离效果。通过多版本并发控制,我们可以解决以下问题:读写之间的阻塞问题。通过MVCC可以防止读写互相阻塞,即读不阻塞写,写不阻塞读。这样可以提高事务的并发处理能力。.死锁的概率降低了。这是因为MVCC采用了乐观锁的方式,读数据时不需要加锁,写操作只锁必要的行。解决连读问题。一致性阅读也称为快照阅读。当我们查询数据库某个时间点的快照时,只能看到该时间点之前事务提交更新的结果,看不到该时间点之后事务提交的更新结果。.2.版本链InnoDB存储引擎的表中,每一行记录都包含一些隐藏字段。下面两个字段对MySQL中MVCC的实现起着重要的作用:db_trx_id:上次操作(插入或更新)这条记录的事务ID。每次事务修改一条记录时,都会将事务的ID赋值给db_trx_id隐藏字段。db_roll_ptr:回滚指针,即指向这条记录的undolog信息。事务每修改一条引用记录,就会将旧版本记录写入undolog,然后db_roll_ptr相当于一个指针,通过它可以找到记录修改前的信息。undolog也称为回滚日志,可以保存事务过程中的数据版本,可以用于回滚,可以提供多版本并发控制(MVCC)下的读操作。例如,有如下成绩单表:CREATETABLE`report`(`id`int(10)UNSIGNEDNOTNULLAUTO_INCREMENT,`name`varchar(10)NOTNULLCOMMENT'name',`score`tinyint(3)UNSIGNEDNOTNULLCOMMENT'Grade',PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBCHARACTERSET=utf8mb4;INSERTINTO`report`VALUES(1,'XiaoMing','70');该表现在只包含一条记录:SELECT*FROMreport;+----+--------+------+|编号|姓名|得分|+----+--------+------+|1|小明|70|+----+------+--------+假设插入这条记录的事务id为20,则有两个事务分别对这条记录进行UPDATE操作,以及事务ID分别为30和40。操作流程如下:transaction30transaction40BEGIN;--BEGIN;UPDATEreportSETscore=80WHEREid=1;-UPDATEreportSETscore=81WHEREid=1;-COMMIT;--UPDATEreportSETscore=90WHEREid=1;-更新报告SETscore=91WHEREid=1;-COMMIT;每改变一条记录,都会记录一个undolog,每个undolog都有一个db_roll_ptr属性(INSERT操作对应的undolog没有这个属性,因为记录没有更早的版本),而这些undologs可以链接在一起形成一个链表,如下图所示:记录更新后,旧值会被放入一个undolog中,保存为旧版本的记录。随着更新次数的增加,所有的版本都会通过db_roll_ptr属性连接成一个链表。我们称这个链表为版本链,版本链的头节点是当前记录的最新值,每个版本也包含对应的TransactionID(db_trx_id)3.ReadView(读视图)对于使用CommittedRead和RepeatableRead隔离级别的事务,必须保证自己读取的是committedtransaction修改的记录,也就是说,如果另一个事务修改了记录但是还没有提交,所以不能直接读取最新版本的记录。核心问题是:需要判断版本链中的哪个版本对当前事务可见。为此,InnoDB提出了ReadView(读视图)的概念,主要包含四个重要属性:m_ids:表示ReadView生成时当前系统中活跃的读写事务的事务id列表。min_trx_id:表示ReadView生成时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。max_trx_id:表示ReadView产生时系统中应该分配给下一个事务的id值。creator_trx_id:表示生成ReadView的事务的事务id。请注意,max_trx_id不是m_ids中的最大值,交易ID是递增分配的。比如有id为1、2、3的三笔交易,然后提交id为3的交易。那么当一个新的读事务产生ReadView时,m_ids包括1和2,min_trx_id的值为1,max_trx_id的值为4。只有当表中的记录发生变化时(执行INSERT、DELETE、UPDATE语句时),transactionid会被赋值给该事务,否则只读事务中的transactionid值默认为0。按照以下步骤可见:如果访问版本的db_trx_id值等于ReadView中的creator_trx_id值,则说明当前事务正在访问自己修改的记录,这样版本才能被当前事务访问到。如果访问的版本的db_trx_id值小于ReadView中的min_trx_id值,说明在当前事务生成ReadView之前,生成该版本的事务已经提交,所以当前事务可以访问到该版本。如果访问的版本的db_trx_id值大于等于ReadView中的max_trx_id值,说明生成该版本的事务是在当前事务生成ReadView之后启动的,所以当前事务不能访问到该版本。如果访问版本的db_trx_id值在ReadView的min_trx_id和max_trx_id之间,那么db_trx_id属性值是否在m_ids列表中?如果是,说明生成这个版本的事务在创建ReadView的时候还处于活动状态,无法访问到这个版本;如果不是,说明生成这个版本的事务在创建ReadView的时候已经提交,可以访问到这个版本。如果某个版本的数据对当前交易不可见,则顺着版本链寻找上一个版本的数据,按照上述步骤继续判断可见性,直到版本链中最早的版本。如果最早版本也是不可见的,则表示该记录对事务完全不可见,查询结果中不包含该记录。ReadView中read-committed和read-repeatable隔离级别的区别在MySQL中,read-committed和read-repeatable隔离级别的一个非常大的区别是它们在不同的时间生成ReadView:read-committed隔离级别是每次执行在正常的SELECT操作之前会生成一个ReadView。可重复读在第一次正常的SELECT操作之前只生成一个ReadView,后续的查询操作会复用这个ReadView。ReadCommitted:在每次读取数据之前生成一个ReadView。对于使用ReadCommitted隔离级别的事务,每次读取数据时都会生成一个ReadView。让我们看看它用一个具体的例子做了什么。我们还是以表格报表为例。现在表report中只有一条记录,最后修改记录的事务ID为40:SELECT*FROMreport;+----+--------+-------+|编号|姓名|得分|+----+--------+------+|1|小明|91|+----+--------+--------+现在系统中有两个事务,事务id分别为50和52:#transaction50BEGIN;UPDATEreportSETscore=70WHEREid=1;UPDATEreportSETscore=71WHEREid=1;#事务52BEGIN;#更新其他表中的一些记录...在事务执行过程中,只有当记录真正被第一次修改时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,该事务id递增。这就是为什么我们在事务52中更新其他表中的一些记录,以便让它分配一个事务id。此时report表中id为1的记录得到的版本列表如下:假设一个使用committedread隔离级别的事务开始执行:#设置当前session的隔离级别为readcommittedSETSESSION事务隔离级别已提交读取;显示类似“transaction_isolation”的变量;+--------------------+------------------+|变量名|值|+--------------------+----------------+|事务隔离|READ-COMMITTED|+------------------------+----------------+#打开一个使用已提交读隔离级别的新事务开始;#SELECT1:事务50,52未提交SELECT*FROMreportWHEREid=1;+----+--------+-------+|编号|姓名|分数|+----+--------+------+|1|小明|91|+----+-------+--------+#得到的columnscore值为91,结合《MVCC数据可见性算法判断流程图》,可以分析出上面SELECT1语句的执行过程如下:SELECT语句执行的时候会生成一个ReadView。ReadView的m_ids列表内容为[50,52],min_trx_id为50,max_trx_id为53,creator_trx_id为0。从版本链中选择可见记录。从图中可以看出,最新版本的score列内容为71,而该版本的trx_id值为50,在m_ids列表中,不符合可见性要求。根据db_roll_ptr上一版本跳转。上一版中score列的内容是70,本版trx_id的值也是50,也在m_ids列表中,所以不符合要求,继续跳到上一版。上一版本score列内容为91,本版本trx_id值为40,小于ReadView中min_trx_id值为50,所以本版本满足要求,最终版本返回给用户是列得分为91的记录。之后,我们提交事务id为50的事务:#transaction50BEGIN;UPDATEreportSETscore=70WHEREid=1;UPDATEreportSETscore=71WHEREid=1;COMMIT;然后去事务id为52的事务更新表report中id为1的记录:#Transaction52BEGIN;#更新其他表中的一些记录...UPDATEreportSETscore=75WHEREid=1;UPDATEreportSETscore=78WHEREid=1;此时表report中id为1的记录的版本链是这样的:然后在刚刚使用committedread隔离级别的事务中继续查找id为1的记录,如下:#使用readcommitted隔离级别开启一个新的事务BEGIN;#SELECT1:事务50,52未提交SELECT*FROMreportWHEREid=1;+----+--------+--------+|id|姓名|得分|+----+--------+------+|1|小明|91|+----+--------+------+#得到的columnscore值为91#SELECT2:transaction50submitted,transaction52notsubmittedSELECT*FROMreport其中id=1;+----+--------+------+|编号|姓名|分数|+----+--------+------+|1|小明|71|+----+--------+-------+#得到的列score的值为71,结合《MVCC数据可见性算法流程图》,可以分析上述SELECT2语句的执行过程如下:因为当前事务的隔离级别是ReadCommitted,所以SELECT语句执行的时候会单独生成一个ReadView。ReadView的m_ids列表内容为[52],min_trx_id为52,max_trx_id为53,creator_trx_id为0。然后从版本链中选择可见的记录。从图中可以看出,最新版本的score列内容为78,而该版本的trx_id值为52,在m_ids列表中,不符合可见性要求。根据db_roll_ptr跳转到上一个版本。上一版中score列的内容是75,而本版的trx_id值为52,也在m_ids列表中,所以不符合要求,继续跳到上一版。上一版本score列内容为71,本版本trx_id值为50,小于ReadView中min_trx_id值52。所以这个版本是符合要求的,最终返回给用户的版本就是该列得分为71的记录。以此类推,如果后面也提交了事务id为52的记录,查询时SELECT*FROMreportWHEREid=1;再次在事务中使用committedread隔离级别,结果列score的值为78,具体过程不再分析。总结一下:使用已提交读隔离级别的事务会在每次查询开始时生成一个单独的ReadView。可重复读:在第一次读取数据时生成一个ReadView。对于使用可重复读隔离级别的事务,只有在第一次执行查询语句时才会生成ReadView,后续查询不会重复生成。.下面用一个具体的例子来看看效果如何。我们还是以表格报表为例。现在表report中只有一条记录,最后修改记录的事务ID为52:SELECT*FROMreport;+----+--------+-------+|编号|姓名|分数|+----+--------+------+|1|小明|78|+----+--------+--------+现在系统中有两个事务,事务id分别为60和62:#transaction60BEGIN;UPDATEreportSETscore=60WHEREid=1;UPDATEreportSETscore=61WHEREid=1;#事务62BEGIN;#更新其他表中的一些记录...此时表report中id为1的记录获取的版本列表如下:假设有一个可重复读隔离级别的事务开始执行:#Set当前会话读取提交的隔离级别--------------+|变量名|值|+----------------------+----------------+|事务隔离|REPEATABLE-READ|+----------------------+----------------+#使用可重复读隔离级别开启一个新的事务BEGIN;#SELECT1:事务60、62未提交SELECT*FROMreportWHEREid=1;+----+--------+--------+|编号|姓名|得分|+----+-------+--------+|1|小明|78|+----+--------+--------+#得到的columnscore值为78结合《MVCC数据可见性算法判断Flowchart”,可以分析出上面SELECT1语句的执行过程如下:执行SELECT语句时,首先会生成一个ReadView,ReadView的m_ids列表的内容为[60,62],min_trx_id为60,max_trx_id为63,creator_trx_id为0从版本链中选取可见记录,从图中可以看出,最新版本的score一栏内容为61,本版本的trx_id值为60,即在m_ids列表中,所以不满足可见性要求。根据db_roll_ptr跳转到上一个版本。上一个版本中score列的内容是60。这个版本的trx_id值也是60,也是在m_ids列表中,所以不符合要求,继续跳转到上一版本,上一版本的columnscore内容为78,本版本的trx_id值为52,小于min_trx_id值ReadView中的60,所以th是版本满足要求,最终返回给用户的版本是该栏目评分为78的记录。之后,我们提交事务id为60的事务,如下所示:#transaction60BEGIN;UPDATEreportSETscore=60WHEREid=1;UPDATEreportSETscore=61WHEREid=1;COMMIT;然后到事务id为62的事务中,更新表report中id为1的记录:#Transaction62BEGIN;#更新其他表中的一些记录...UPDATEreportSETscore=65WHEREid=1;UPDATEreportSETscore=68WHEREid=1;此时表report中id为1的记录的版本链是这样的:然后使用repeatableread隔离级别继续在刚才打开的事务中查找id为1的记录,如下:#使用repeatable读隔离级别开始一个新的事务BEGIN;#SELECT1:事务60,62未提交SELECT*FROMreportWHEREid=1;+----+--------+--------+|编号|姓名|分数|+----+--------+------+|1|小明|78|+----+-------+--------+#得到的columnscore值为78#SELECT2:transaction60submitted,transaction62notsubmittedSELECT*FROMreport其中id=1;+----+---------+------+|编号|姓名|分数|+----+--------+------+|1|小明|78|+----+--------+--------+#得到的columnscore的值还是78,上面SELECT2语句的执行过程如下:因为隔离当前事务级别为可重复Read,执行SELECT1之前已经生成了ReadView,所以此时直接复用之前的ReadView。前面ReadView的m_ids列表内容为[60,62],min_trx_id为60,max_trx_id为63,creator_trx_id为0。然后从版本链中选择可见的记录。从图中可以看出,最新版本的score列内容为68,而本版本的trx_id值为62,在m_ids列表中,不符合可见性要求。根据db_roll_ptr跳转到上一个版本。上一版本中score列内容为65,而本版本中trx_id的值为62,也在m_ids列表中,所以不符合要求,继续跳到上一版本。上一版本score列内容为61,本版本trx_id值为60,m_ids列表中包含值为60的transactionid,所以本版本不符合要求,内容为nextcolumnscore同样是60的版本也不符合要求。继续跳转到上一版本。上一版本score列内容为78,本版本trx_id值为52,小于ReadView中min_trx_id值为60,所以本版本满足要求,最终版本返回给用户是列得分为78的记录。也就是说,两次SELECT查询的结果是重复的,recordscore的值为78,也就是repeatableread的意思。如果我们后面提交事务id为62的记录,然后使用可重复读隔离级别事务继续查找id为1的记录,结果还是78分。