图解MySQL MVCC

图解MySQL MVCC

1.MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

2.基本思想

加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。

3.版本号

  • 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号 TRX_ID :事务开始时的系统版本号。

4.Undo 日志

MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。

例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。

INSERT INTO t(id, x) VALUES(1, "a");
UPDATE t SET x="b" WHERE id=1;
UPDATE t SET x="c" WHERE id=1;

因为没有使用 START TRANSACTION 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。

在这里插入图片描述
INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。

5.ReadView

VCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有 TRX_ID_MINTRX_ID_MAX

TRX_ID_MIN :是当前已经提交的事务号 + 1, 事务号 < up_limit_id ,对于当前Read View都是可见的。理解起来就是创建Read View视图的时候,之前已经提交的事务对于该事务肯定是可见的。

TRX_ID_MAX:当前最大的事务号 + 1,事务号 >= low_limit_id,对于当前Read View都是不可见的。理解起来就是在创建Read View视图之后创建的事务对于该事务肯定是不可见的。

TRX_IDs :为活跃事务id列表,即Read View初始化时当前未提交的事务列表。

当 TRX_ID_MIN <= 事务号 <= TRX_ID_MAX

  • 提交读:如果 事务号 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
  • 可重复读:都不可以使用, 等于自己事务ID除外。TRX_ID_MIN <= 事务号 <= TRX_ID_MAX 时表示当前快照时该事务还未提交, 如果事务号不存在,意味着是在当前事务开启后再提交的,而可重复读不能读取事务开启后其他事务提交的结果。

在这里插入图片描述
Read Committed隔离级别:每次select都生成一个快照(Read View)读。Read Repeatable隔离级别:只在开启事务后生成快照读。

6.图例

我们来举个图例,让我们更好理解上面的内容。

比如我们有如下表:
在这里插入图片描述

现在有一个事务id是60的执行如下语句并提交:

update user set name = '强哥1' where id = 1;

此时undo log存在版本链如下:
在这里插入图片描述
提交事务id是60的记录后,接着有一个事务id为100的事务,修改name=强哥2,但是事务还没提交。则此时的版本链是:
在这里插入图片描述
此时另一个事务发起select语句查询id=1的记录,此时Read View如下:
在这里插入图片描述
因为trx_ids当前只有事务id为100的,所以该条记录不可见,继续查询下一条,发现trx_id=60的事务号小于up_limit_id,则可见,直接返回结果强哥1。

那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录name=强哥3,并且不提交事务。这时候版本链就是:
在这里插入图片描述
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。

如果你是已提交读隔离级别READ_COMMITED,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是强哥2。
在这里插入图片描述
如果你是可重复读隔离级别REPEATABLE_READ,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是强哥1。所以第二次select结果和第一次一样,所以叫可重复读!

也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。

7.时间轴图例

我们再从一个带有时间轴的图例来进行理解:
在TRX_ID = 102事务中执行查询语句,此时的Undo log 与 Read View如图所示,因此只能读在该事务开启之前已提交的事务,即60。
在这里插入图片描述
什么情况会出现 TRX_ID > TRX_MAX_ID 这种情况呢?

如下图:
在这里插入图片描述
在②处执行select的时候,就会从undo log中 TRX_ID 为 105 的事务开始进行比较,最后可以看出读到的仍然是 TRX_ID 为 60的事务数据,满足可重复读。

文章中有理解错误的地方,欢迎指正!
参考文档:
1.数据库系统原理
2.一文理解Mysql MVCC

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值