MVCC多版本并发控制

MVCC (Multiversion Concurrency Control) 中文全程叫多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。

一、undolog

我们在进行数据更新操作的时候,不仅会记录 redolog 日志,而且也会记录 undolog 日志,如果因为某些原因导致事物回滚,那么这个时候 MySQL 就需要回滚保证事物的一致性,使用 undolog 将数据恢复到事物开始之前的状态。

例如删除一条数据:

delete from T where id = 1;

此时 undolog 日志就会记录一条对应的 insert 语句(反向操作的语句) ,保证在事物回滚的时候,可以把数据还原回去。

insert into T(id) values (1)

更新一条数据:

---修改之前name=张三
update user set name = "李四" where id = 1;

undolog 日志就会记录一条相反的 update 语句;

update user set name = "张三" where id = 1;

undolog 日志是 MVCC 多版本并发控制重要的数据来源

二、视图

在 MySQL 里,有两个视图的概念:

  • 一个是 view。它是一个用于查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果,创建视图的语法是create view... 而它的查询方式跟表一样
  • 另外一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC 和 RR 隔离级别的实现

三、MVCC 实现原理

1. 数据版本可见性规则

在 InnoDB 中,每一行记录都有三个隐藏列:trx_id,roll_ptr,db_row_id(如果没有主键,则还会多一个隐藏的主键列)

  • trx_id:记录最近更新这条记录的事务 ID
  • roll_ptr:表示指向改行回滚段的指针,InnoDB 通过整个指针找到之前的版本,该行记录上所有的旧版本,在 undolog 中通过链表的形式组织
  • db_row_id:行标识(隐藏单调自增 ID)

而且在 InnoDB 里面每个事务都有一个唯一的事务ID,叫做 transaction id,它是在事务开启的时候向事务系统申请的,是按申请顺序严格递增

每次事务更新的时候,都会生成一个新的版本数据,并且把 transaction id 赋值给整个数据版本的事务 ID,即上面的trx_id,同时旧的版本要保留,通过roll_ptr直接找到上一个版本

以下就是一条记录被多个事务变更后的流程:

图中是同一行数据的四个版本,当前最新版本是 V4,k=22,他是被 transaction id 为 25 的事务更新的,因此他的 trx_id=25,其中虚线部分就是用 roll_ptr 指针来串联起来的 undolog 日志

图中的 V1,V2,V3 并不是物理上真实存在的,而是每次需要的时候,根据当前版本和 undolog 日志计算出来的,比如我现在要 V3 的数据,就需要从 V4 找出上个版本然后返回

InnoDB 为每个事务构造了一个数组,称之为 m_ids,用来保存这个事务启动瞬间,当前正在活跃的所有事务ID活跃指的是事务启动了,但还没有提交

数组里面事务 ID 的最小值为 low_limit_id,最大值为 up_limit_id + 1,这个视图数组和最大值最小值就组成了当前事物的一致性视图,而数据版本的可见性就是基于数据的 trx_id 和这个一致性视图对比结果得到的

这样,对于当前事务的启动瞬间来说,一条记录的数据版本trx_id,有以下几种可能:

  1. 如果在绿色部分,表示这个版本是已提交事物的或者是当前事务自己生成的,这个数据是可见的
  2. 如果是红色部分,表示这个版本是由将来的事务生成的,是不可见的
  3. 如果在黄色部分,那就有两种可能
    a. 若trx_id在活跃事务数组中,则表示这个版本是由没提交的事务生成的,不可见
    b. 若trc_id不在活跃事务数组中,则表示这个版本是已经提交了的事务生成的,可见

2. 案例实践

idkrow_idroll_ptr
1199xxx

这里我们不妨做下假设:

  1. 事务A开始前,系统里只有一个活跃事务ID是99
  2. 事务A,B,C的版本分别是100,101,102,且当前系统里只有这四个事务
  3. 三个事务开始前,(1,1)这一行数据的trx_id是90

这样事务A的活跃事务数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]


从图中可以看出,第一个有效更新是事务C,把数据k=1改成了k=2,这时候,数据最新版本trx_id = 102,而trx_id=90则成为了历史版本
第二个有效更新是事务B,把数据k=2改成了k=3,这时候数据最新版本trx_id=101,而102又成为了历史
这个时候A事务要来读取数据,见第8行,事务A的视图数组是[99,100],当然读数据都是从当前版本读起,所以事务A查询语句的读取数据流程是:

  1. 找到k=3的时候,判断trx_id=101,在事务A的视图数据最大值 100+1,处于红色区域不可见
  2. 接着根据roll_ptr的指针,从undolog日志中找到上一个版本trx_id = 102,也是处于红色区域,不可见
  3. 继续往前找,找到trx_id=90,此时比A事务视图数组最低值还要小,处于绿色区域,所以是可见的,所以事务A查询的数据k=1

问题:但是如果按照一致性读,事务B在第6行的更新中,不是应该看到k=1嘛,算出来应该k=2,为什么会算出k=3呢?

是的,如果在事务B更新之前,也就是第6行之前查询,k确实是1,但是当事务B要去更新数据的时候,就不能在历史的版本上更新了,否则C事务的更新就丢失了,因此事务B此时的set k = k+1是在k=2的基础上操作的
所以这里就引出了这样一条规则: 更新数据都是先读后写,而这个读,只能读当前的值,称为当前读,当然只能读取到提交的数据
所以在更新的时候,当前读拿到的k=2,更新后k=3,这时候trx_id = 101,当第7行事务B查询语句的时候,发现trx_id=101,自己的版本号也是101,是自己的更新,可以直接使用,所以第7行查出来的k=3

一个版本数据,对于一个事务视图来说,除了自己更新的总可以见以外,有三种情况:

  1. 版本未提交,不可见
  2. 版本已提交,但是是在视图创建后提交的,不可见
  3. 版本已提交,而且是在视图创建前提交的,可见

以上都是基于可重复读隔离级别来看数据的,那么如果隔离级别是读已提交下,事务A和事务B查询到的k,分别又是多少呢?

事务A的查询语句,第9行的视图数组是在执行这个语句的时候创建的,看图上k=2,k=3都在事务A查询语句之前,但是这个时候k=3还没有提交,属于情况1,不可见,k=2已经提交了,属于情况3,可见,所以第9行查询出来的是k=2
当然,事务B查询出来的k=3

四、总结

可重复读的核心就是一致性读,而事务更新数据的时候,只能用当前读如果当前记录的行锁被其他事务占用的话,就需要进入锁等待
而都提交的逻辑和可重复读逻辑类似,他们的主要区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

我是一零贰肆,一个关注Java技术和记录生活的博主。

欢迎扫码关注“一零贰肆”的公众号,一起学习,共同进步,多看路,少踩坑。

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值