MySQL的MVCC概念介绍(转)
并发字段修改业务
最近在主要在做“工作流引擎”课题的预研工作,在涉及到“会签任务”(工作流业务概念,这与我们今天讨论文问题没有太多关联)的时候,遇到了一个并发修改同一个字段的应用场景。
大致是由于要等一个活动节点的所有实例任务都完成之后才能继续向下流转,则引擎必须在每次任务提交的时候进行判断。我选择了在数据库表中记录下每个活动节点对应的任务实例数目,活动实例完成提交时做相应的数目修改(active_ti_num - 1)来进行对应活动节点是否完成的判断。数据库表结构如下:
活动表字段名 | id(活动主键) | ai_name(活动名称) | active_ti_num(当前活动未完成实例个数) |
---|---|---|---|
示例数据 | 1213398753365504001 | 活动1 | 1 |
任务表字段名 | id(任务主键) | ai_id(对应活动id,外键) |
---|---|---|
示例数据 | 1213400206226272258 | 1213398753365504001 |
如上所示,当同一个活动具有多个任务实例的时候,而任务实例又并发完成,就可能由于并发update导致数据错误,所以我将任务实例提交处理封成了一个事务,再使用update自减的方式修改active_ti_num字段值。
<update id="decrementActiveNum" parameterType="int">
UPDATE wf_activtity_instance
SET active_ti_num = active_ti_num + 1
WHERE id = #{id}
</update>
这样在第一个事务修改了active_ti_num后,会锁住活动表中被修改的这一行,其他的事务便只能等待,等持有锁的事务锁释放之后,其他事务可以竞争锁再进行active_ti_num字段修改,从而保证了不出现数据错误。这种处理方法也是一种比较常见的处理方法。
啰啰嗦嗦说了这么多,业务问题虽然解决了,但不知道大家有没有过疑惑,虽然为了保证数据不发生错误,修改的数据被锁住了,但是MySQL究竟加的是行锁还是表锁?如果我们遇到的是并发insert操作而非update,那是否会出现新的问题?想解决这些疑惑,就需要引出我们今天的话题——“MVCC原理与在InnoDB中的实现”
MVCC概念介绍
在并发操作的控制上,MySQL的大多事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,他们一般都同时实现了MVCC(多版本并发控制)。可以认为MVCC是行级锁的一个变种,在很多场景下避免了加锁操作,因此开销更低。工作在 RC (读已提交)、RR(可重复度)两种隔离级别下。至于这个MVCC究竟是怎么做到既保证效果,又提高并发的,我们先来看看《高性能MySQL》中的介绍。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。MVCC是通过每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间)。当然实际存储的不是时间而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号。
对于SELECT操作,就查找版本早于当前事务版本的数据行,行的删除版本要么未定义,要么大于当前事务版本。
对于INSERT操作,InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
对于DELETE操作,Innodb为删除的每一行保存当前系统版本号作为行删除标识。
对于UPDATE操作,Innodb为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
以上是MVCC实现的一个大致概括,各存储引擎具体实现上还是略有不同。由于InnoBD是MySQL默认的存储引擎,也是我项目使用的存储引擎,因此我们就来看看在InnoBD中MVCC的实现原理与作用是怎样的(其他存储引擎笔者也不会是吧...)。
InnoDB中MVCC的实现思路
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC ,一条记录除了包括各个字段值,还包括了当前事务id(trx_id)和一个指针(roll_pointer)。
- trx_id:生成这条记录(update/delete)的事务id
- roll_pointer:之前undo_log中原来的那条记录,从而构成版本链
注:一个事务的事务id在第一次insert/delete/update时生成
我们接下来通过具体操作的实现思路来进行讲解:
Update操作
插入一条新的记录,把原来的记录放到undo日志中去,再把新纪录的roll_pointer指针指向原来的那条记录(从而加入版本链)
Select操作
当执行查询sql时会生成一致性视图read-view,它由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id( max_id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果(即从版本链头部记录开始,顺着链开始比对,找到可见的第一个版本记录)。
版本链比对规则
如果落在绿色部分( trx_id< min_id),表示这个版本是已提交的事务生成的,这个数据是可见的;
如果落在红色部分( trx_id> max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的。
如果落在黄色部分( min_id<=trx_id<= max_id),那就包括两种情况
a.若row的trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见,当前自己的事务是可见的。
b.若row的trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见
delete操作
对于删除的情况可以认为是 update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息( record header)里的( deleted flag)标记位写上true,来表示当前记录已经被刪除,在查询时按照上面的规则查到对应的记录如果 delete flag标己位为true,意味看记录已被删除,则不返回数据。
知道了MVCC的实现机制,那现在我们可以思考下MVCC是如何实现可重复读的和读已提交的呢?
MVCC是如何实现可重复读的和读已提交的?
可重复读隔离级别下,SELECT一致性视图(readview)沿用第一次生成的(这是mvcc实现可重复读的关键,即使其他事务commit,但由于readview还是第一次select时生成的那个,所以当前事务还是看不到),而读已提交隔离级别下,每次SELECT操作生成最新的一致性视图(readview)
注:readview是在当前会话(事务)第一条sql语句执行时生成的,在可重复读的隔离级别下,后面的语句都沿用这个readview(也就是说生成的readview是查哪个表用都有效的)
由此可见,可重复读也解决了幻读问题,因为新插入的记录的trx_id肯定会出现在select事务readview的未提交事务id数组/大于最大事务id,所以对于该事务肯定不可见,从而解决了幻读问题。
到这可能有读者会疑惑,之前说的都是对于读数据的并发控制,可是你的业务是更新啊!这还不是一回事啊!
别急,接下来我们就要说到啦!
快照读与当前读的区别?以及在MVCC中的应用
咦?怎么读还有两个?
“读”与“读”的区别
我们且看,在RR(可重复读)级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。(比如说并发情况下自增或者先读再增(更新值对原数据值有依赖性))
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。
快照读其实就是普通的select操作,如
select * from table ….;
当前读则是特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
由此我们可以想到,事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。记下来,我们详细来说说当前读
当前读(“写”)
事务的隔离级别中虽然只定义了读数据的要求,实际上这也可以说是写数据的要求。上文的“读”,实际是讲的快照读;而这里说的“写”就是当前读了。
读问题在上文中已经解决了,根据MVCC的定义,并发提交数据时会出现冲突,那么冲突时如何解决呢?我们再来看看InnoDB中RR级别对于写数据的处理。
InnoDB使用了Next-Key锁解决当前读中的幻读问题。首先我们看下什么是Next-Key锁。
Next-key Lock:锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。
Record Lock:对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
Gap Lock:对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
接下来我们可以看看RR级别和RC级别的对比,来体会Next-key锁的作用。
RC级别:
RR级别:
通过对比我们可以发现,在RC级别中,事务A修改了所有teacher_id=30的数据,但是当事务Binsert进新数据后,事务A发现莫名其妙多了一行teacher_id=30的数据,而且没有被之前的update语句所修改,这就是“当前读”的幻读。
RR级别中,事务A在update后加锁,事务B无法插入新数据,这样事务A在update前后读的数据保持一致,避免了幻读。这个锁,就是Gap锁。
InnoDB是这么实现的:
在class_teacher这张表中,teacher_id是个索引,那么它就会维护一套B+树的数据关系。
而InnoDB使用的是聚集索引,teacher_id身为二级索引,就要维护一个索引字段和主键id的树状结构,学过数据结构的同学都会知道,在树节点内部关键字保持顺序排列如下图(意会)。
如上图索引结构,Innodb将这段数据分成几个个区间
(negative infinity, 5],
(5,30],
(30,positive infinity);
update class_teacher set class_name=‘初三四班’ where teacher_id=30;不仅用行锁,锁住了相应的数据行;同时也在两边的区间,(5,30]和(30,positive infinity),都加入了gap锁。这样事务B就无法在这个两个区间insert进新数据。
因此,受限于这种实现方式,Innodb很多时候会锁住不需要锁的区间。如下图所示
update的teacher_id=20是在(5,30]区间,即使没有修改任何数据,Innodb也会在这个区间加gap锁,导致事务B必须等待,而其它区间不会影响,事务C正常插入。
此外,如果(where条件)使用的是没有索引的字段,比如update class_teacher set teacher_id=7 where class_name=‘初三八班(即使没有匹配到任何数据)’,那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的幻读问题。
总结
MVCC不可重复读的保证其实是由快照读和当前读两个方面着手,快照读通过mvcc的版本控制来解决,不需要真正加锁。当前读通过行锁和GAP锁(锁的范围为索引B+树中当前索引两边的区间,要是没有索引就锁表)结合形成的的Next-Key锁来解决不可重复度和幻读的问题。
参考资料
《高性能MySQL》第三版
MySQL的MVCC概念介绍(转)的更多相关文章
- 理解MySQL——架构与概念
写在前面:最早接触的MySQL是在三年前,那时候MySQL还是4.x版本,很多功能都不支持,比如,存储过程,视图,触发器,更别说分布式事务等复杂特性了.但从5.0(2005年10月)开始,MySQL渐 ...
- 【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之集群概念介绍(一)
集群概念介绍(一)) 白宁超 2015年7月16日 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习 ...
- 大并发大数量中的MYSQL瓶颈与NOSQL介绍
NoSQL在2010年风生水起,大大小小的Web站点在追求高性能高可靠性方面,不由自主都选择了NoSQL技术作为优先考虑的方面.今年伊始,InfoQ中文站有幸邀请到凤凰网的孙立先生,为大家分享他之于N ...
- 转载:【Oracle 集群】RAC知识图文详细教程(一)--集群概念介绍
文章导航 集群概念介绍(一) ORACLE集群概念和原理(二) RAC 工作原理和相关组件(三) 缓存融合技术(四) RAC 特殊问题和实战经验(五) ORACLE 11 G版本2 RAC在LINUX ...
- 数据库 MySQL 之 基本概念
数据库 MySQL 之 基本概念 浏览目录 概述 数据库的特点 数据库的分类 选择MySQL的理由 & MariaDB 介绍 下载及安装 SQL介绍 一.概述 1.数据(data) 存储在表中 ...
- 【转】【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之集群概念介绍(一)
原文地址:http://www.cnblogs.com/baiboy/p/orc1.html 阅读目录 目录 集群概念介绍 什么是集群 为什么搭建数据库集群 数据库集群的分类 可扩展的分布式数据库架构 ...
- 《Mysql - 事务 MVCC》
一:前言 - 前面通过 <Mysql 事务 - 隔离> 的学习,知道了事务的实现,是根据 获取一致性视图 来实现的. 二:那么,什么时候会获取到一致性视图呢? - 例如:有三个事务,启动的 ...
- Mysql部分存储引擎介绍
Mysql存储引擎 什么是存储引擎 mysql中建立的库 --> 文件夹 库中建立的表 --> 文件 现实生活中我们用来存储数据的文件有不同的类型,每种文件类型对应各自不同的处理机制: ...
- 【MySQL】面试官:谈谈你对Mysql的MVCC的理解?
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制.MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问. 在Mysql的In ...
- java面试一日一题:讲对mysql的MVCC的理解
问题:请讲下对mysql中MVCC的理解 分析:这个问题要回答的是对MVCC的理解,以及MVCC解决了什么问题这几个方面入手. 回答要点: 主要从以下几点去考虑, 1.什么是MVCC? 2.MVCC用 ...
随机推荐
- JS - console多个值
Promise.all([p1, p2]).then( (res) => { let [p1, p2] = res; console.l ...
- [转帖]Redis子进程开销与优化
Redis子进程开销与优化 文章系转载,便于分类和归纳,源文地址:https://blog.csdn.net/y532798113/article/details/106870299 1.CPU 开销 ...
- 关于IO性能的一些学习与了解
关于IO性能的一些学习与了解 摘要 最近心气不高. 学习进度也拖的比较慢. 以后想能够多为自己着想.自己有自己的节奏, 不能只为别人考虑. 要改变一下自己的做事风格. 一些事情想帮则帮, 不想帮就当看 ...
- Docker 安装Oracle12c的镜像修改字符集 并且进行启动的简单过程
学习来自 昨天晚上转帖的文章 这里面添加一些自己的内容 首先获取配置文件 git clone https://github.com/oracle/docker-images.git 获取之后比较容易了 ...
- 麒麟信安V3.4 安装PG15的过程V2.0
改版说明 上一次进行了PG的安装当时发现自己少加了一部分ossp的处理. 这里补充上...不然挺浪费时间的. 背景 发现基于OpenEuler的几个系统使用CentOS的rpm包 安装PG数据库时有问 ...
- Docker 运行 Redis Rabbitmq seata-server ftp 的简单办法
公司里面用到了很多组件, 发现安装二进制太麻烦了, 所以想用Docker 进行安装. 这里面简单给总结一下就不在折腾了.. 1. redis docker run -d -p 6379:6379 -- ...
- MAT的简单学习
背景说明 Java遇到问题之后比较浅层的跟踪解决办法: jps 查看进程的main jar包 对应的进程信息 jstack 查看 堆栈信息 top -Hp PID 实时查看具体的CPU进程信息. 如果 ...
- 使用systemd管理多nginx服务以及单nginx服务实现多vhost访问的操作步骤
背景 nginx是开源的web服务器, 性能与可配置性和插件做的非常完善. 可以使用简单的命令拉起来nginx进行服务提供,但是有时候需要使用keepalive等软件实现保活,以及实现开启启动等,比较 ...
- 获取文件的后缀名(转为数组) 字符串和变量的拼接 HTML中字符串和变量的拼接
1文件上传时,获取文件的后缀名### var cont="2010-23.23.xls" console.log(cont.split("."));//spli ...
- 你不知道的Promise状态变化机制
1.Promise中PromiseStatus的三种状态 var p = new Promise((resolve, reject) => { // resolve 既是函数也是参数,它用于处理 ...