版本链
基于undo log
对于使用InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
trx_id
:每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id
隐藏列。roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
假设之后两个id
分别为100
、200
的事务对这条记录进行UPDATE
操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志
,每条undo日志
也都有一个roll_pointer
属性(INSERT
操作对应的undo日志
没有该属性,因为该记录并没有更早的版本),可以将这些undo日志
都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。
ReadView
实现快照读
- 核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。
ReadView
主要包含m_ids
,存储当前活跃的事务id。
READ COMMITTED
每次读取数据前都生成一个ReadView,查找版本链中事务id小于当前活跃事务id。代价高
比方说现在系统里有两个id
分别为100
、200
的事务在执行:
# Transaction 100
BEGIN;
UPDATE t SET c = '关羽' WHERE id = 1;
UPDATE t SET c = '张飞' WHERE id = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
此刻,表t
中id
为1
的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITTED
隔离级别的事务开始执行:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'
这个SELECT1
的执行过程如下:
- 在执行
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[100, 200]
。 - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。 - …
- 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,小于m_ids
列表中最小的事务id100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'刘备'
的记录。
REPEATABLE READ
在第一次读取数据时生成一个ReadView
比方说现在系统里有两个id
分别为100
、200
的事务在执行:
# Transaction 100
BEGIN;
UPDATE t SET c = '关羽' WHERE id = 1;
UPDATE t SET c = '张飞' WHERE id = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
此刻,表t
中id
为1
的记录得到的版本链表如下所示:
假设现在有一个使用REPEATABLE READ
隔离级别的事务开始执行:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'
这个SELECT1
的执行过程如下:
- 在执行
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[100, 200]
。 - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。 - …
- 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,小于m_ids
列表中最小的事务id100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'刘备'
的记录。
之后,我们把事务id为100
的事务提交一下
然后再到事务id为200
的事务中更新一下表t
中id
为1的记录:
# Transaction 200
BEGIN;
UPDATE t SET c = '赵云' WHERE id = 1;
UPDATE t SET c = '诸葛亮' WHERE id = 1;
此刻,表t
中id
为1
的记录的版本链就长这样:
然后再到刚才使用REPEATABLE READ
隔离级别的事务中继续查找这个id为1
的记录,如下:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值仍为'刘备'
这个SELECT2
的执行过程如下:
- 因为之前已经生成过
ReadView
了,所以此时直接复用之前的ReadView
,之前的ReadView
中的m_ids
列表就是[100, 200]
。 - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'诸葛亮'
,该版本的trx_id
值为200
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 下一个版本的列
c
的内容是'赵云'
,该版本的trx_id
值为200
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。 - 下一个版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,而m_ids
列表中是包含值为100
的事务id的,所以该版本也不符合要求,同理下一个列c
的内容是'关羽'
的版本也不符合要求。继续跳到下一个版本。 - 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,80
小于m_ids
列表中最小的事务id100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'刘备'
的记录。