本文转载自微信公众号《菜鸟飞天》,作者刘金坤。转载本文请联系菜鸟飞亚飞公众号。undologversionchain在MySQL数据表中逐行存储数据记录。对于每一行数据,它不仅记录了我们定义的字段值,还隐藏了两个字段:row_trx_id和roll_pointer。前者表示update行数据的事务id,后者表示回滚指针,指向该行数据上一版本的undolog(不明白这是什么的可以继续往回看).每行有两个隐藏字段。它们在《高性能 MySQL》第三版第13页被称为数据更新时间和过期时间。这两个字段存储的不是实时,而是交易的版本号。这与本文中的row_trx_id和roll_pointer的名称有很大不同。其实不用管这两个字段叫什么名字。反正都是为了实现MVCC机制而设计的。个人认为分别叫作row_trx_id和roll_pointer会更容易理解。我们知道,当我们对数据进行增删改查时,会写入redolog(解决数据库宕机重启导致数据丢失的问题)和binlog(主要用于复制、数据备份等),同时也写undolog,就是实现事务的回滚操作。各个undolog的具体内容本文不再赘述。有兴趣的同学可以上网查一下。我们只需要知道undolog的每一行都会记录对应的事务id,当前事务修改的数据的最新值,以及指向当前行数据的上一版本undolog的指针,即滚动指针。为了便于理解,undolog的每一行可以简化为下图所示的结构:图1例如现在有一个事务A,事务id为10,向其中插入了一条新的数据表,数据记录为data_A,那么此时对应的undolog应该如下图所示:图2是新插入的一条数据,所以这行数据是第一个版本,即它没有以前的数据版本,所以它的roll_pointer是空的。然后事务B(trx_id=20),将这一行数据的值修改为data_B,同时记录一条undolog,如下图,这条undolog的roll_pointer指针会指向前面的undologdataversion,同时也指向事务A写入的undolog那一行。接下来图3,事务C(trx_id=30),将这行数据的值修改为data_C,对应示意图如下.图4只要有事务修改了这一行的数据,就会记录相应的undolog。一个undolog对应这行数据的一个版本。当这行数据有多个版本时,就会有多个undologlog和undolog通过roll_pointer指针连接起来,这样就形成了undolog版本链ReadView机制,当一个事务开始执行时,会产生一个ReadView对于每笔交易。这个ReadView会记录4个很重要的属性:creator_trx_id:当前交易的id;m_ids:当前系统中所有活跃事务的id,活跃事务是指当前系统中已经开启但尚未提交的事务;min_trx_id:当前系统中,所有活跃交易中交易id最小的交易为m_id数组中最小的交易id;max_trx_id:当前系统中交易id值最大的交易id加1,即下一个要生成的交易id。ReadView会根据这4个属性实现MVCC机制,结合undolog版本链,决定一个事务哪些数据可以读取,哪些数据不能读取。那么它是如何实现的呢?如果用一个坐标轴来表示,min_trx_id和max_trx_id会将坐标轴分为三部分:图5当一个事务读取一条数据时,会根据以下规则判断当前事务可以读取哪些数据:如果当前数据的row_trx_id小于min_trx_id,说明这个数据是在当前事务开始之前,其他事务已经修改了数据并提交了事务(事务的id值是自增的),所以当前事务可以读取它。如果当前数据的row_trx_id大于等于max_trx_id,说明当前事务开启后,经过一段时间后,系统中开启了新的事务,新的事务修改了这一行的值的数据并提交事务,所以当前事务肯定是不可读的,所以这是后续事务修改提交的数据。如果当前数据的row_trx_id在min_trx_id和max_trx_id范围之间,有两种情况:(a)row_trx_id在m_ids数组中,则无法读取当前事务。为什么?m_ids数组中的row_trx_id表示与当前事务同时开启的事务,修改了数据的值,提交了事务,所以无法读取当前事务;(b)row_trx_id不在m_ids数组中,则可以读取当前交易。如果row_trx_id不在m_ids数组中,说明在当前事务开始之前,其他事务已经提交了修改数据后的事务,所以当前事务可以读取。注意:如果row_trx_id等于当前事务的id,说明这条数据是被当前事务修改的,必须当前事务能够读取。这里可能有人会有疑问,交易的id值是递增的,那么在什么场景下,row_trx_id在min_trx_id和max_trx_id之间,而不在m_id数组中呢?这个问题也困扰了我很久,最近终于弄明白了,答案是在readcommit的事务隔离级别下会出现这种现象。至于为什么,需要看这篇文章和下一篇文章《在读提交的事务隔离级别下,MVCC 机制是如何工作的?》才能明白为什么。下面通过几个例子来说明ReadView机制下的数据读取规则。首先假设表中有一条数据,它的row_trx_id=10,roll_pointer为null,那么此时的undolog版本链如下图所示:图6假设有事务A和事务B并发执行,事务A的事务id为20,事务B的事务id为30。那么此时对于事务A,在其ReadView中,m_ids=[20,30],min_trx_id=20,max_trx_id=31,creator_trx_id=20。对于事务B,在其ReadView中,m_ids=[20,30],min_trx_id=20,max_trx_id=31,creator_trx_id=30。如果此时事务A(trx_id=20)读取数据,那么在undolog版本链中,最新版本数据的事务id为10,小于事务A的ReadView中min_trx_id的值,表示这个数据的版本是在事务A开始之前被其他事务提交的,所以事务A可以读到,所以读到的值为data0。图7跟随事务B(trx_id=30)修改数据,将数据修改为data_B,先不提交事务。虽然事务没有提交,但是还是会记录一条undolog,所以这条数据的undolog版本链中有两条记录,新的undolog的roll_pointer指针会指向之前的undolog,如图下图。图8跟随事务A(trx_id=20)读取数据,那么在undolog版本链中,最新版本数据的事务id为30,这个值在事务A的ReadView中介于min_trx_id和max_trx_id之间,所以需要判断这个数据版本的值是否在m_ids数组中,结果发现30确实在m_ids数组中,也就是说这个版本的数据被和自己同时启动的事务修改了,所以这个版本的数据,数据A是读不出来的。所以需要沿着undolog的版本链向前查找,就会找到该行数据的上一个版本,也就是trx_id=10的版本。由于该版本数据的trx_id=10小于min_trx_id的值,事务A可以读取到该版本的值,即事务A读取的值为data0。图9后面是事务B提交事务,那么此时系统中唯一活跃的事务就是id为20的事务,也就是事务A。那么此时事务A再次读取数据,什么值可以它读?它仍然是data0。为什么?虽然系统中只剩下id为20的active事务,但是事务A打开的那一刻,已经生成了ReadView。即使后面有其他事务提交,事务A的ReadView也不会被修改,即m_ids不会改变,或者m_ids=[20,30],所以此时事务A根据undolog读取数据时versionchain,还是读取不到最新版本的数据,只能向前看,最后只能读取到data0。然后在系统中,新开了一个事务C,事务id为40。在它的ReadView中,m_ids=[20,40],min_trx_id=20,max_trx_id=41,creator_trx_id=40。然后事务C(trx_id=40)修改数据到data_C,提交事务。此时undolog版本链变成如下图所示。图10此时事务A(trx_id=20)要读取数据,那么在undolog版本链中,最新版本数据的事务id为40,因为事务A的ReadView中max_trx_id=31此时40大于31,这说明当前版本的数据是在事务A之后提交的,所以事务A肯定是读不到的。所以此时事务A只能根据roll_pointer指针沿着undolog版本向前看。原来之前版本的trx_id=30是自己读取不到的,所以继续往前看,终于可以读取到trx_id=10版本的数据,所以事务A最后只能读取到data0。图11然后事务A(trx_id=20)修改数据,修改数据到data_A,然后会记录一个undolog,示意图如下:图12然后事务A(trx_id=20)读取数据,在undolog版本链中,数据的最新版本的事务id为20,对比事务A,发现这个版本的事务id等于自己的事务id,也就是说这个版本的数据是自己修改的。既然是自己修改的,那么肯定是读取的,所以此时读取的是data_A。图13总结综上所述,本文主要讲解undolog版本链是如何形成的,然后讲解ReadView的机制是什么。通过几个例子和画图,详细分析了ReadView结合undolog版本链是如何实现的。当前事务读取哪个版本的数据是MVCC机制的核心实现原理。但是到目前为止,我只分析了ReadView和undolog是如何实现MVCC机制的,如何控制事务读取数据的方式,MVCC机制在特定的事务隔离级别下是如何工作的。
