Mysql中的MVCC到底是什么

MVCC 是什么

多版本并发控制,是一种mysql在多事务访问下的解决线程安全问题的一种无锁解决方案。主要原理是利用undo log 形成版本链,利用undo log 中记录了不同事务产生的记录和事务id 去判断记录的可见性。

事务在并发情况下可能出现的问题

  • 读读
  • 读写 脏读、幻读、不可重复读问题。
  • 写写 可能出现覆盖更新问题。

事务隔离级别

  • 读未提交
  • 读以提交
  • 可重复读
  • 串行化

不同隔离级别下解决的问题

  • 读未提交(read uncommit) 允许读取到事务未提交的数据 可能导致脏读、不可重复读、幻读
  • 读已提交(READ COMMITTED) 允许事务读取到已提交的数据可能导致不可重复读、幻读
  • 可重复读(REPEATABLE READ)
    对同一字段多次读取的结果都是一致的、除非是被本身事务所修改。
  • 串行化 解决幻读

undo log

前面提到了mvcc原理是利用undo log 形成版本链,利用undo log 中记录了不同事务产生的记录和事务id 去判断记录的可见性来实现的,具体undo log 形成的版本链类似于这样的形式。(事务id 的生成是递增的)

trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的id赋值给trx_id隐藏列.

roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改之前的信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YewTbY6b-1691564281374)(en-resource://database/554:0)]
图片来源
MVCC详解,深入浅出简单易懂_lans_g的博客-CSDN博客

每次记录更新后,都会将旧值写入到undolog中,随着修改次数的增多,我们通过roll pointer 形成了一个链表,也就是版本链。版本链的头结点就是当前记录最新的数据。每个版本的数据中还携带着生成该版本数据时对应的事务id。

Read View

ReadView 是针对于使用 READ COMMITTED 和 REPEATABLE READ 这两种隔离级别来描述的,都必须读取到其他事务已经提交的了数据。这里的核心逻辑就是如何判断版本链中的数据对哪个事务是可见的。 于是出现了Read View 这个概念。
ReadView 中包含了四个比较重要的概念:

  • m_ids 表示生成Read View 时当前系统中活跃的读写事务的事务id 列表
  • min_trx_id : 表示生成Read View时,当前系统中活跃的读写事务中最小的事务id。也就是m_ids 中的最小值
  • max_trx_id: 表示生成ReadView时系统中应该分配给下一个事务的id值。(这里的max_trx_id 并不是指m_ids中的最大id,
    事务id是递增分配的。比方说现在有id为1,2,3这三 个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,mi n_trx_id的值就是1,max_trx_id的值就是4。)
  • createor_trx_id: 表示生成该事务的事务id。

在有了ReadView 的情况下,在访问某条记录时,我们只需按照如下流程判断记录的可见性即可。

  • 如果被访问版本的trx_id 与ReadView中的creator_trx_id 一致。表示他在访问自己修改的数据,所以当前版本数据被该事务可见。
  • 如果被访问版本的trx_id 小于ReadView 中的min_trx_id 表示该版本数据在ReadView生成前已提交,所以当前版本数据被该事务可见。
  • 如果被访问版本的trx_id 大于ReadView 中的max_trx_id 表示该版本数据在ReadView生成后生成 所以当前版本数据被该事务不可见。
  • 如果被访问版本的trx_id 处于ReadView 中的min_trx_id 与 max-trx_id之间。则需要判断当前trx_id 是否存在于m_ids中。
    如果存在,则证明当前事务修改的事务还未提交,所以当前版本数据被该事务不可见。
    如果不在,说明创建ReadView 时生成该版本的事务已经被提交,当前版本的数据对该事务可见。

Read View 在Read Committed、Repeated read 下的区别

在MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的 时机不同。

READ COMMITTED

在每次读取数据前都生成一个ReadView


# Transaction 100 
BEGIN; 
UPDATE hero SET name = '关羽' WHERE number = 1; 
UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录

此刻,表hero 中 number 为 1 的记录得到的版本链表如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEOU0OFi-1691564281375)(en-resource://database/556:0)]

假设现在有一个使用READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200未提交 
SELECT * FROM hero WHERE number = 1; #  得到的列name的值为'刘备'


这个SELECT1 的执行过程如下:

  • 在执行SELECT 语句时会先生成一个ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] ,min_trx_id 为 100 , max_trx_id 为 201 , creator_trx_id 为 0 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列name 的内容是’关羽’,该版本的trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。

之后,我们把事务id为100的事务提交一下,就像这样:

# Transaction 100 
BEGIN; 
UPDATE hero SET name = '关羽' WHERE number = 1; 
UPDATE hero SET name = '张飞' WHERE number = 1; 
COMMIT;

然后再到事务id为200 的事务中更新一下表hero 中number 为 1 的记录:

# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
... 
UPDATE hero SET name = '赵云' WHERE number = 1; 
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

此刻,表hero 中 number 为 1 的记录的版本链就长这样:
在这里插入图片描述

然后再到刚才使用READ COMMITTED 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200均未提交 
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备' 
# SELECT2:Transaction 100提交,Transaction 200未提交 
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'

这个SELECT2 的执行过程如下:

  • 在执行SELECT 语句时会又会单独生成一个 ReadView ,该 ReadView 的 m_ids 列表的内容就是 [200] ( 事务id 为 100 的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id 为 200 ,max_trx_id 为 201 , creator_trx_id 为 0 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据roll_pointer 跳到下一个版本。
  • 下一个版本的列name 的内容是’赵云’,该版本的trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name 的内容是’张飞’,该版本的trx_id 值为 100 ,小于 ReadView 中的 min_trx_id 值
    200 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’张飞’的记录。

以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED 隔离级别的事务中查询表
hero 中 number 值为 1 的记录时,得到的结果就是 ‘诸葛亮’ 了,具体流程我们就不分析了。总结一下就是:
使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

REPEATABLE READ

只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。

对于使用REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView ,之后的查
询就不会重复生成了。我们还是用例子看一下是什么效果。
比方说现在系统里有两个事务id分别为100、200的事务在执行:

# Transaction 100 
BEGIN; 
UPDATE hero SET name = '关羽' WHERE number = 1; 
UPDATE hero SET name = '张飞' WHERE number = 1;
 # Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
...

此刻,表hero 中 number 为 1 的记录得到的版本链表如下所示:

此刻,表hero 中 number 为 1 的记录得到的版本链表如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyGIe8Az-1691564281375)(en-resource://database/560:0)]

假设现在有一个使用REPEATABLE READ 隔离级别的事务开始执行:

# 使用REPEATABLE READ隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200未提交 
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

这个SELECT1 的执行过程如下:

  • 在执行SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] ,min_trx_id 为 100 , max_trx_id 为 201 , creator_trx_id 为 0 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列name 的内容是’关羽’,该版本的trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。

之后,我们把事务id为100的事务提交一下,就像这样:

# Transaction 100 
BEGIN; 
UPDATE hero SET name = '关羽' WHERE number = 1; 
UPDATE hero SET name = '张飞' WHERE number = 1; 
COMMIT;

然后再到事务id为200 的事务中更新一下表hero 中number 为 1 的记录:

# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
... 
UPDATE hero SET name = '赵云' WHERE number = 1; 
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

此刻,表hero 中 number 为 1 的记录的版本链就长这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aml9TCSx-1691564281376)(en-resource://database/562:0)]

然后再到刚才使用REPEATABLE READ 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:

# 使用REPEATABLE READ隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200均未提交 
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备' 
# SELECT2:Transaction 100提交,Transaction 200未提交 
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为'刘备'

这个SELECT2 的执行过程如下:

  • 因为当前事务的隔离级别为REPEATABLE READ ,而之前在执行 SELECT1 时已经生成过 ReadView 了,所以此时直接复用之前的ReadView ,之前的 ReadView 的 m_ids 列表的内容就是 [100, 200] , min_trx_id 为
    100 , max_trx_id 为 201 , creator_trx_id 为 0 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的
    trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列name 的内容是’赵云’,该版本的trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name 的内容是’张飞’,该版本的trx_id 值为 100 ,而 m_ids 列表中是包含值为 100 的事务id 的,所以该版本也不符合要求,同理下一个列name的内容是’关羽’的版本也不符合要求。继续跳
    到下一个版本。
  • 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为’刘备’的记录。

也就是说两次SELECT 查询得到的结果是重复的,记录的列c值都是’刘备’,这就是可重复读的含义。如果我们之后再把事务id为200 的记录提交了,然后再到刚才使用REPEATABLE READ 隔离级别的事务中继续查找这个number 为 1 的记录,得到的结果还是’刘备’ 。

参考:Mysql 是怎么运行的:从根上理解mysql

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值