深入理解MySQL的MVCC机制

什么是MVCC?

MVCC(多版本并发控制),实现并发访问的一种方式,在mysql的Innodb引擎读已提交和可重复读两种隔离级别下,事务在select时实际上读取的是版本链中的数据。

MVCC只在Innodb引擎下,事务隔离级别为读已提交和可重复读时才存在

简单来理解,你可以想象一下Java中的写时复制(CopyOnWrite),其实思想都是差不多的,为了能够做到读写不互斥,所以在每次写入时,总是先copy一份,读请求还是读的原来的,而写请求时写的copy出来的,写完之后在用copy的覆盖原来的。

那么MVCC的实现思想也是一样的,通过生成多个版本的方式,来控制当前读请求已经读哪个版本,写请求又应该写哪个版本。

版本链

刚刚有提到了版本链,那么这个版本链又是什么呢?实际上就是多个版本之间的一种关联关系。

在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:

1、trx_id

这个id用来存储的每次对某条聚簇索引记录进行修改时候的事务id。

roll_pointer

每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

比如现在有一个trx_id = 1,那么当修改时,则会在undo日志中新增一条记录,trx_id = 2,roll_pointer = (trx_id = 1的地址)

对某一行的age列进行更新操作,并且不提交事务,多个版本之间形成如下图的这种关系,就是版本链。
在这里插入图片描述

ReadView

刚刚我们说了MVCC只对读已提交和可重复读两种隔离级别下有意义,现在我们就来看看这两种隔离级别下分别是如下实现的。

我们先来回顾下,在读已提交和可重复读下产生的不同效果。

读已提交

会产生:不可重复读、幻读

不可重复读演示

A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 1; --假设结果为zhangsan
B:start transaction;
B:update tran_test set name = 'zs' where id = 1; --把id为1的name修改为zs
A:select * from tran_test where id = 1; -- B未提交所以A此时查询结果还是zhangsan
B:commit;
A:select * from tran_test where id = 1; -- B事务提交后,A再查询此时结果已经变成了zs
可重复读

只会产生幻读,不会产生不可重复读

幻读演示

A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 4; --结果为空
B:start transaction;
B:insert into tran_test values(4,'zhaoliu'); -- B插入一条id=4的数据
A:select * from tran_test where id = 4; --结果依旧为空
B:commit; -- B提交事务
A:select * from tran_test where id = 4; --结果依旧为空
A:insert into tran_test values(4,'zhaoliu'); -- 插入失败,报主键冲突

实际上对于不可重复读的问题就是通过ReadView来解决的。

解决不可重复读的方式

ReadView会先通过一个链表来记录当前已开启但还未提交的trx_id(假设我们叫这种事务为:当前活动事务)。

举例说明:

当前trx_id = 3,修改age=2,此时事务还未提交,版本链如下:

在这里插入图片描述
当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。

现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:
在这里插入图片描述
ReadView通过一个链表来记录当前已开启但还未提交的trx_id(当前活动事务)。

举例说明,在读已提交的场景下:

当前trx_id = 3,修改age=2,此时事务还未提交,版本链如下:

在这里插入图片描述
当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。

现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:
在这里插入图片描述
现在关键部分来了,看看读已提交和可重复读两种不同隔离级别的做法。

读已提交

当另一个事务再次select时,会重新生成一次ReadView,由于当前活动事务id为4,所以ReadView中记录的trx_id = 4,然后再根据之前的方式,最终会查询到age = 3的记录。

可重复读

当另一个事务再次select时,并不会重新生成ReadView,而是继续使用上一次的ReadView,那么ReadView中记录的当前活动事务id就还是为3,那么最终还是查询到age = 2的记录。

所以说在读已提交的隔离级别下,每次select时都会重新生成一个ReadView,而在可重复读时,只会在第一次select时生成ReadView,之后事务中的每次select,都只会复用第一次生成的ReadView。

总结

MVCC实际上就是为了能够让读写不互斥的一种解决方式,就如同Java中的CopyOnWrite,linux中的fork函数,Redis的RDB,后3者也都是写时复制的思想,MVCC也就是做了同样的事情,只不过具体的实现方式有点区别。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值