什么是 MVCC
Multiversion Concurrency Control:多版本并发控制,提供并发访问数据库时,对事务内读取的到的内存做处理,用来避免写操作堵塞读操作的并发问题(提高并发读写性能)。
MVCC 主要适用于 Mysql 的 RC(读已提交),RR(可重复读)隔离级别
问题(痛点)
A 正在读数据库中某些内容,而 B 正在给这些内容做修改(A,B 为两个单独的事务),A 可能看到一个不一致的数据,在 B 没有提交前,如何让 A 能够一直读到的数据都是一致的或读到的数据一直是最新的呢?
一个支持MVCC的数据库,在更新某些数据时,并非使用新数据覆盖旧数据,而是标记旧数据是过时的,同时在其他地方新增一个数据版本。因此,同一份数据有多个版本存储,但只有一个是最新的。每个用户连接数据库时,看到的都是某一特定时刻的数据库快照,在 B 的事务没有提交之前,A 始终读到的是某一特定时刻的数据库快照,不会读到 B 事务中的数据修改情况,直到 B 事务提交,才会读取 B 的修改内容。
MVCC 提供了 时间一致性的 处理思路,在 MVCC 下读事务时,通常使用一个时间戳或者事务 ID 来确定访问哪个状态的数据库及哪些版本的数据。读事务跟写事务彼此是隔离开来的,彼此之间不会影响。假设同一份数据,既有读事务访问,又有写事务操作,实际上,写事务会新建一个新的数据版本,而读事务访问的是旧的数据版本,直到写事务提交,读事务才会访问到这个新的数据版本。
InnoDB 的 MVCC 实现机制
- 每条记录都会保存两个隐藏列,
事务id
(trx_id)和回滚指针
(roll_point)。 - 每次操作都会生成一条 undo log 日志,回滚指针指向前一条记录。
版本链
`roll_pointer`每次对记录修改的时候,都会把老版本写入undo log中。新纪录中`roll_pointer`就是存了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。测试表结构如下:
INSERT
操作对应的undo日志
没有该属性,因为该记录并没有更早的版本。假设之后两个
事务id
分别为100
、200
的事务对这条记录进行UPDATE
操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志
,每条undo日志
也都有一个roll_pointer
属性,可以将这些undo日志
都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id,这个信息很重要,我们稍后就会用到。ReadView
对于使用
READ UNCOMMITTED
隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE
隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED
和REPEATABLE READ
隔离级别的事务来说,就需要用到我们上边所说的版本链
了,核心问题就是:<font color=#A52A2A size=4 > 需要判断一下版本链中的哪个版本是当前事务可见的。所以后面提出了一个ReadView
的概念,<font color=blue size=4 > 这个ReadView
中主要包含当前系统中还有哪些活跃的(未提交的)读写事务,把它们的事务 id 放到一个列表中,我们把这个列表命名为为m_ids
和表示生成该 ReadView 的快照读操作产生的事务 id(creator_trx_id
)。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
<font color=red size=3 >
-
如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
<font color=#C71585 size=3 > -
如果被访问版本的
trx_id
属性值小于m_ids
列表中最小的事务 id,表明生成该版本的事务在生成ReadView
前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的
trx_id
属性值大于m_ids
列表中最大的事务 id,表明生成该版本的事务在生成ReadView
后才生成,所以该版本不可以被当前事务访问。
- 如果被访问版本的
trx_id
属性值在m_ids
列表中最大的事务 id 和最小事务 id 之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
在
MySQL
中,READ COMMITTED
和REPEATABLE READ
隔离级别的的一个非常大的区别就是它们生成ReadView
的时机不同。
READ COMMITTED --- 每次读取数据前都生成一个 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
的记录得到的版本链表如下所示:
假设现在有一个使用 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
值也为100
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。 - 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,小于m_ids
列表中最小的事务 id100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'刘备'
的记录。
之后,我们把事务 id 为 100
的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE t SET c = '关羽' WHERE id = 1;
UPDATE t SET c = '张飞' WHERE id = 1;
COMMIT;
然后再到事务 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
的记录的版本链就长这样:
然后再到刚才使用 READ COMMITTED
隔离级别的事务中继续查找这个 id 为 1
的记录,如下:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'张飞'
这个 SELECT2
的执行过程如下:
- 在执行
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[200]
。 - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'诸葛亮'
,该版本的trx_id
值为200
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 下一个版本的列
c
的内容是'赵云'
,该版本的trx_id
值为200
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。 - 下一个版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,比m_ids
列表中最小的事务 id200
还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'张飞'
的记录。
以此类推,如果之后事务 id 为 200
的记录也提交了,再此在使用 READ COMMITTED
隔离级别的事务中查询表 t
中 id
值为 1
的记录时,得到的结果就是 '诸葛亮'
了,具体流程我们就不分析了。<font color=Crimson size=3 > 总结一下就是:使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。
REPEATABLE READ
---在第一次读取数据时生成一个 ReadView
对于使用
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
值也为100
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。 - 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,小于m_ids
列表中最小的事务 id100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'刘备'
的记录。
之后,我们把事务 id 为 100
的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE t SET c = '关羽' WHERE id = 1;
UPDATE t SET c = '张飞' WHERE id = 1;
COMMIT;
然后再到事务 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;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'
# 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
为'刘备'
的记录。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于