首页 > 其他分享 >MVCC多版本并发控制

MVCC多版本并发控制

时间:2024-04-02 16:23:38浏览次数:27  
标签:事务 trx 视图 并发 版本 MVCC 数据 id

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. 案例实践

id k row_id roll_ptr
1 1 99 xxx

这里我们不妨做下假设:

  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技术和记录生活的博主。

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

标签:事务,trx,视图,并发,版本,MVCC,数据,id
From: https://www.cnblogs.com/sun2020/p/18110844

相关文章

  • nvm管理node版本后手动安装npm包管理
    在命令行npm检查检查过node后,输入指令npm-v来检查npm是否安装成功。毕竟node8以上的版本,就不支持npm的自动安装了。如果显示npm版本号,说明npm也安装成功。如果显示npm非内部指令或外部指令等字样,说明npm并没有自动安装。npm安装失败的处理失败的原因很简单。如果正......
  • 高并发技术具体实现
    1、线程池使用线程池可以有效地管理和复用线程,减少线程创建和销毁的开销,提高系统的并发处理能力。2、异步编程,响应式编程采用异步编程模型可以减少线程的阻塞时间,提高系统的吞吐量和响应性能。可以使用Java8引入的CompletableFuture或者基于事件驱动的框架(如Netty)来实现异步编......
  • Elasticsearch各个版本重要特性
    Elasticsearch各个版本重要特性Elasticsearch5Elasticsearch6.0Elasticsearch7.0Elasticsearch8.0Elasticsearch5首先说明下,ES是从版本2直接跳到5的,主要是为了和ElasticStack其他组件保持版本一致ES5,在现在来说是比较老的版本了,就不多介绍了建议大家使用ES7,或者直接使用ES8E......
  • ios 之 netty版本swiftNio(socket创建)
    SwiftNio简介用于高性能协议服务器和客户端的事件驱动、无阻塞的网络应用程序框架。SwiftNIO是一个跨平台异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。这就像Netty,但是为Swift写的。Xcode引入swiftNio        在实际写代码前,......
  • Java并发-如何避免死锁
    一般在Java项目里用到锁的场景不多,有朋友调侃说用到锁的次数还没有面试被问到的次数多,哈哈!1、死锁如何产生说句难听话,锁一般都很少用到,何况死锁呢?想产生死锁还是有点难的,需要满足2个条件:共享资源同时只能被一个线程使用,如果已经有一个线程占用了资源,其余线程只能等待,直到资......
  • 高并发下的数据一致性保障(图文全面总结)
    1背景我们之前介绍过分布式事务的解决方案,参考作者这篇《五种分布式事务解决方案(图文总结)》。在那篇文章中我们介绍了分布式场景下困扰我们的3个核心需求(CAP):一致性、可用性、分区容错性,以及在实际场景中的业务折衷。1、一致性(Consistency):再分布,所有实例节点同一时间看到是相......
  • 2024最新一线互联网大厂常见高并发面试题解析
    面试官:临界区是什么?答:临界区用来表示一种公共资源或者说是共享资源,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。比如,在一个办公室里有一台打印机,打印机一次只能执行一个任务。如果小王和小明同时需要打......
  • IDEA中新建SpringBoot模块,JDK版本问题解决
    问题描述IDEA中新建SpringBoot模块,使用的JAVAJDK1.8,新建模块时选项中没有JDK8: 运行时报错,JDK之类的问题解决方案,查看修改以下四个地方:(1)设置-Java编译器 (2)项目结构--依赖以及源码 ......
  • Har 版本包发布
    新建Module注意名字最好是小写命名oh-package.json5修改description描述信息在Index.ets导出对外暴露的组件export{TitleBar}from'./src/main/ets/view/TitleBar' 编译生成har包 生成公钥和私钥新建文件夹,新建空的pub文件。命令生成公钥和私钥......
  • 下载安装 macOS 版本的 Windows 远程桌面客户端(Microsoft Remote Desktop)
    如果有非国区的账号,直接在商店中下载即可:https://apps.apple.com/us/app/microsoft-remote-desktop/id1295203466?mt=12国区是搜不到的,微软提供了beta版本下载:https://install.appcenter.ms/orgs/rdmacios-k2vy/apps/microsoft-remote-desktop-for-mac/distribution_groups/al......