有一天,一群好朋友要各奔东西了,小明最先走,他在走之前给大伙合影,以后看不到大伙(里的小红)了,只能通过看相片来回忆了,尤其是小红那一张清纯的面容。当照片定格的那一刻,小伙伴(小红)的面容在小明心里留下的印象就是照片中那样了。
即使后来,小红去整容了,或者被车撞了破相了,小明心目中小红的印象永远是照片中清纯的模样,因为小红破相的事情是发生在小明跟大伙拜拜之后了。
MySQL中的可重复读就是这种思想,当一个事务A创建时,事务A只能看见A开始前已经提交的事务搞出来的内容,在A开启后,其他事务再对某个记录搞来搞去搞出别的花样,A都是无法看见他们搞了什么的。
在可重复读隔离级别下,每次启动事务都会创建一个事务的id,专业来讲是Read View,Read View就跟快照一样,是某个的时刻数据库定格的模样。
Read View的组成
Read View就像是做编程题时维护的一个类,是每个事务都独享的一个局部变量。里面有四个变量:creator_trx_id
、m_ids
、min_trx_id
、max_trx_id
,这四个变量都是在事务创建时就被赋了值且不会改变的了。含义分别是:
creator_trx_id
:创建当前Read View的事务idm_ids
:这个变量应该是一个列表,储存了当前正在活跃的事务id,即已经开启了但还未提交的事务min_trx_id
:m_ids
里的最小事务id,这个变量的作用应该是快速判断一条记录能否被当前事务所读到,即这条记录是否在事务开启前就存在的。下面还会讲到这个字段的作用,可以先不用理解max_trx_id
:m_id
里最大的事务id+1
可以先来感性的认识m_ids
这个字段,因为min_trx_id
和max_trx_id
都是为了快速判断的,是可以根据m_ids得到的。m_ids里面储存的是在这个Read View创建时还未提交的事务。所以当我们创建某个事务A时,m_ids列表中的事务产生的新的记录对于当前事务A肯定是不可见的。否则就违背了RR隔离级别的规则(前后两次读到的记录不一样)。所以,一旦看到某条记录是由m_ids列表中的一个事务搞出来的,我们立马就可以下结论:这条记录的这个版本是当前事务不可见的,要顺着版本链往下找到不在m_ids中的事务id弄出来的记录(即当前事务开启前就已经提交的事务搞出来的记录)。版本和版本链是什么下面会提到。
那么,对于某事务弄出来的新记录,我们是如何标识呢?
每条记录的两个隐藏字段
聚簇索引是每张表都有且只能有一个的索引,所以在聚簇索引上,我们可以对每条记录都记录一下这个记录是由谁弄出来的,这就是每条记录都有的两个隐藏字段:trx_id
、roll_pointer
。
trx_id很好理解,就是标识这条记录是由哪个事务搞出来的。但是光有这个还不够,因为对于每条记录,都会有不同的事务反反复复在修改记录的内容,所以不同事务将这条记录修改成了什么样子,我们都要将其记录下来,每个事务修改并提交的内容视为是一个版本。对于同一个记录的不同版本,我们应该按版本的新旧把不同版本都串起来,这就形成了一条版本链。另外一个隐藏字段roll_point
就是版本链中的链条,就跟C语言链表的next指针那样,负责将各个点串成一个链条。
有了Read View和每条记录记录的版本号,我们就能判断事务能读到记录的哪一个版本。有了版本链,事务就能顺着版本链读到对它可见的那个版本的记录。
下面是例子:!
事务52创建时,事务51还没有提交。所以直观上来看,就算51把记录1改了并提交了,52也看不到51改的内容,而是只能看到50弄出来的记录1。MVCC确实是做到了这点。
- 51改了记录1然后提交,记录1就会有两个版本,分别是50和51,这时候
roll_point
就发挥作用,将两个记录串成一个版本链 - 52想去看记录1时,看到记录1的trx_id是51,它想了想,51在我的Read View的m_ids中,所以51弄出来的东西我是不可见的
- 既然不可见,那我就到顺着版本链找一个我能看的。然后找到了50,我能看,然后读出来balance=1000000
总结
MVCC就是基于版本号来管理事务的。每个事务知道自己创建时其他事务的状态,每条记录维护了不同版本的版本链。事务根据自己创建时其他事务的状态读出能够被自己读到的某个版本的记录。