MVCC和快照读
首先我们回忆一下事务的隔离级别MySQL的事务和隔离级别实战,在RR(Repeatable Read,可重复读)中每一次读的都是事务启动时保存的快照,这样才能不管其他事务如何更新,都不影响当前事务读取的结果。那么这里就引入了一个重要的概念——快照。
快照和版本链
在InnoDB中每条记录会额外添加DB_TRX_ID
和DB_ROLL_PTR
两列。
-
DB_TRX_ID
:一个事务每次对某条主键索引记录进行改动时,会把该事务的事务ID赋值给DB_TRX_ID
。 -
DB_ROLL_PTR
:每次改动时会把记录的旧版本写入到undo log,这个隐藏列相当于一个指针,它指向对应的undo log,通过DB_ROLL_PTR
我们就可以回溯该记录的历史修改。
现在我们可以理解,undo log就相当于快照,DB_ROLL_PTR
可以将这些undo log串成一个链表,这个链表就是版本链。
MVCC
利用版本链我们就可以控制不同事务访问同一条记录时,应该用哪一个版本,这就是多版本并发控制(Multi-Version Concurrency Control, MVCC)。那么,该记录的哪一个版本是当前事务可见呢?这就又引出了一个重要的概念——一致性视图(ReadView)。在一致性视图里有4个比较重要的内容:
活跃:启动了但还没有提交
-
m_ids
:生成一致性视图时,当前系统活跃的读写事务的事务ID列表 -
min_trx_id
:生成一致性视图时,当前系统中活跃的读写事务中最小的事务ID,也就是m_ids
中的最小值。 -
max_trx_id
:在生成一致性视图时,系统应该分配给下一个事务的事务ID值。计算方式就是当前系统里面已经创建过的事务ID的最大值加1。 -
creator_trx_id
:生成该一致性视图的事务的事务ID(只有对表中的记录进行改动时,即insert、delete、update操作,才会为事务分配唯一的事务ID,否则一个事务的事务ID默认为0)。
现在,在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本是否可见:
-
如果该版本的
DB_TRX_ID
<min_trx_id
,代表该版本对应的事务在当前事务生成一致性视图前已经提交,所以该版本可以被当前事务访问。 -
如果该版本的
DB_TRX_ID
>max_trx_id
,代表该版本对应的事务在当前事务生成一致性视图后才开启,所以该版本不可以被当前事务访问。 -
如果该版本的
DB_TRX_ID
∈[min_trx_id
,max_trx_id
],则需要判断DB_TRX_ID
是否在m_ids中。如果在,说明当前事务在生成一致性视图时,该版本对应的事务仍处于活跃状态,那么该版本不可以被当前事务访问;如果不在,说明当前事务在生成一致性视图时,该版本对应的事务已经被提交,该版本可以被当前事务访问。
大家可以暂停阅读思考一下,如果不能被当前事务访问,应该怎么办?
答案是:顺着版本链往下找到下一个版本的数据,并利用上面的步骤判断记录的可见性,直至找到可见版本或者返回空。
需要注意的是,不同时机生成的一致性视图里面的内容是不一样的,所以开启一个事务后,理解该事务到底选择的是什么时机生成的一致性视图是十分重要的。在MySQL中,最常用的读已提交(Read Committed, RC)和可重复读(Read Repeatable, RR)两种事务隔离级别根本区别就在于生成一致性视图的时机不同。下面我们以该表为例说明两种隔离级别的区别:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
可重复读
事务A | 事务B | 事务C |
---|---|---|
begin; | begin; | |
update t set c = 1 where id = 0; | update t1(另一个表) set c =1 where id = 0; | |
update t set c = 2 where id = 0; | ||
begin; | ||
【查询1】select * from t where id = 0; |
在事务B中更新另一个表的数据主要是为了产生一个事务ID。还记得吗?上面我们提到只有对表中的记录进行改动时,即insert、delete、update操作,才会为事务分配唯一的事务ID,否则一个事务的事务ID默认为0。假设事务A的事务ID为10,事务B的事务ID为20,则事务C的【查询1】过程如下:
-
在执行select时会产生一个一致性视图,其
m_ids
的内容就是[10,20],creator_trx_id
为0。 -
从上图可以看出,c的最新版本为2,
DB_TRX_ID
的值为10,在m_ids
中,因此不满足可见条件,需要顺着DB_ROLL_PTR
往下找。 -
下一个版本的
DB_TRX_ID
的值还是为10,继续往下寻找。 -
最后找到
DB_TRX_ID
值为5的版本,满足可见条件,返回该记录,c=0。
事务A | 事务B | 事务C |
---|---|---|
commit; | ||
update t set c = 3 where id = 1; | ||
update t set c = 4 where id = 1; | ||
【查询2】select * from t where id = 0; |
接下来,我们将事务A提交,然后尝试在事务B中更新t表id=1这条记录。此时得到的版本链如下图所示:
事务C的【查询2】执行过程如下:
-
在RR级别下,只有第一次select会产生一致性视图,所以
m_ids
的内容还是[10,20]。 -
从上图可以看出,c的最新版本为4,
DB_TRX_ID
的值为20,在m_ids
中,因此不满足可见条件,需要顺着DB_ROLL_PTR
往下找。 -
最后找到
DB_TRX_ID
值为5的版本,满足可见条件,返回该记录,c=0。
可以看出来,在RR级别下,【查询1】、【查询2】得到的结果是一样的,这就是可重复读的含义。
读已提交
我们完全可以复用上述例子来分析RC级别下【查询1】、【查询2】的结果。首先可以确定的是,【查询1】RC和RR的读取结果是一样的。但是,在【查询2】的时候,RC会再生成一次一致性视图,这样RC级别下【查询2】的执行过程如下:
-
此时事务A已提交,重新生成一致性视图时,
m_ids
的内容是[20]。 -
从之前的图可以看出,c的最新版本为4,
DB_TRX_ID
的值为20,在m_ids
中,因此不满足可见条件,需要顺着DB_ROLL_PTR
往下找。 -
但是在找到
DB_TRX_ID
的值为10的记录时,发现10不在m_ids
中,则该版本记录是可见的。所以停止寻找,返回该记录,c=2。
可以看出来,在RC级别下,【查询1】、【查询2】得到的结果是不一样的,事务C会读取已经提交事务更新后的结果,这就是读已提交的含义。
快照删除
-
对于insert产生的undo log,在该事务提交之后就可以删除了
-
对于update产生的undo log,比如上面例子中的事务A产生的undo log,在事务A提交后不能立即删除,因为事务C在【查询2】的执行过程中还会访问事务A产生的undo log。
什么时候可以删除update产生的undo log?
【简单理解】当系统中最早产生的那个一致性视图不再需要访问它们,即
m_ids
中没有或者小于min_trx_id
,那么这些update undo log就可以删除了。
【详细分析】事务提交时,会给事务生成一个名为事务 no
的值,先提交的事务的事务 no
更小。一致性视图中也有事务 no
这个属性,生成一致性视图的时候,把当前系统中最大的事务 no
,加1赋值给这个属性。执行purge时把系统中最早的一致性视图取出来,然后从各个回滚段的History链表中取出事务no较小的各组undo log,如果这组undo log的事务 no
值小于最早一致性视图的事务 no
值,那么就可以彻底删除释放了。
以上面的例子为例,在事务A提交前,产生的最早一致性视图是事务C查询得到的,保存的事务 no
假设为101+1=102,事务A提交后对应undo log的事务 no
也是102,所以只有当事务C提交了之后,最早的一致性视图的事务 no
才可能大于102,此时事务A对应的undo log才可以彻底删除。
如今互联网上各类文章满天飞,但是大部分要不是寥寥数语,让人过目即忘;要不是过多细枝末节又没有实操,让人不知所云。我将从个人学习和工作经历出发,给大家带来深入浅出的技术解析。我的文章力求简短精悍,尽量结合实战,以便大家在碎片时间即可充分吸收,后续还能学以致用。
欢迎大家关注我的微信公众号,所有文章第一时间更新~