概念
MVCC(Multiversion Concurrency Control):多版本并发控制。是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
MVCC的实现思路
-
修改数据时,都对应一个修改者所属的事务【前提条件】
-
每个事务都有自己的事务编号【前提条件】
-
每条数据里都有个隐藏字段,可以用来存修改者事务编号【前提条件】
-
每次修改时,保留原数据,产生一条新版本数据。并且在新数据里的隐藏字段存上修改者事务编号A【前提条件】
-
当另一个事务B要去访问那个数据时,就可以根据访问者的事务编号B去判断并选取哪个版本的数据【实现MVCC】
-
至于怎么选取,还有一个“场外配置项”来控制:隔离级别【配置】
至此我们就知道了MVCC基本原理。
mysql实现的MVCC逻辑
见文章Mysql的MVCC的理解(这篇文章简洁而逻辑清晰,就不重复了)
这里就着重讲解以下几点:
四种隔离级别:
- RU(READ UNCOMMITTED)读未提交
- RC(READ COMMITTED)读已提交
- RR(REPEATABLE READ)可重复读
- SERIALIZABLE 串行化
版本链:就是用来把各个版本的数据连起来,保存起来。供访问者从新到旧的顺序筛选,直到找到自己能看的数据
ReadView:一个非常重要且比较难理解的概念。表示当前活跃的事务id列表(已开始,还未提交),这是个静态数据,生成了就不再变。
根据我们前面说的mvcc基本原理。大致的朴素设想:隔离级别应该是用来控制算法走向的。
但mysql的实现更加巧妙。算法中用到了ReadView这个数据,隔离级别是用来控制ReadView的。实现了算法不变,算法用到的数据根据配置改变(优美的代码结构)。
逻辑场景:ReadView是访问者产生的。每次访问数据,访问者自己就生成一个ReadView,拿在手里。然后去版本链里筛选数据。
隔离级别的控制方式:RC的时候,是整个事务里,每次select都会重新产生一个ReadView。RR的时候,是访问者第一次select时生成了ReadView,整个事务里一直用它(虽然RR的隔离级别更高,但相对来说实现起来反而更简单,更快。默认用这个级别大概也有层意思)。
注意:上面的“select”指的是“select 同一张表”(RC时,即便两次单独查不同行,也会使用同一个ReadView。但查不同表就一定是两个ReadView)。
实验
这是一个很经典的观察ReadView生成逻辑的简单实验。
实验材料:
-
MySql5.7
-
一个表,名叫staff。字段:id | name | age | pos | salary
操作:开两个命令行窗口,都打开msql命令行窗口,分别开两个事务A,B;A读 B改数据
目标:观察一个可能会被误解的现象(如果懂了上面的原理,就很容易理解了)
前提:
-
关掉事务自动提交
set global autocommit=0;
-
保持mysql的默认隔离级别RR不要动
实验1(命令前的数字代表操作顺序号)
事务A | 事务B |
---|---|
1. begin; 【开始事务A】 | 2. begin;【开始事务B】 |
3. select * from staff;【观察到:id为3的age=20】 | |
4. update staff set age=30 where id=3; | |
5. commit;【结束事务B】 | |
6. select * from staff;【观察到:id为3的age=20】 |
这个现象很好理解,符合我们心中RR的印象:一个事务里,查询到的数据不变,所以事务A感知不到B对数据的修改。
实验2(对照组)
事务A | 事务B |
---|---|
1. begin; 【开始事务A】 | 2. begin;【开始事务B】 |
3. update staff set age=40 where id=3; | |
4. commit;【结束事务B】 | |
5. select * from staff;【观察到:id为3的age=40】 | |
6. commit;【结束事务A】 |
这个现象就可能不太符合我们对RR的理解了。事务A居然感知到B对数据的变化了。
实际上这是我们对RR的误解,RR的本意是:可重复读。即在一个事务里,无论读多少次都一样。并没有规定读到的是什么数据。
如果不了解原理,可能会觉得很神奇,甚至有点“量子力学”到感觉:是否观察居然影响到了结果。
实验2中之所以能读到事务B修改的数据,那是因为事务A在步骤5时才第一次生生成ReadView。
如果要验证RC的情况,就先改一下配置隔离级别:
//设置read uncommitted级别:
set session transaction isolation level read uncommitted;//设置read committed级别:
set session transaction isolation level read committed;//设置repeatable read级别:
set session transaction isolation level repeatable read;//设置serializable级别:
set session transaction isolation level serializable;
如果要验证:“select”指的是“select 同一张表”。可以把两个select * from staff;换成不同的表,换成单独查两行,都是可以验证的。
至此,你已经理解mysql实现mvcc的关键原理。
深入细节
有了前面的铺垫,可以再看这篇文章(MySQL中MVCC的正确打开方式(源码佐证)),就不会有太大难度了。没啥可强调的,其实就是上面关键原理的具体实现代码层面。有兴趣的可以看看。
MVCC和二级索引
上面我们讲的数据版本链,都是数据行上的操作。那如果远离数据行的二级索引被改了,肯定也要读写并行。那么是怎么实现的?
其实上二级索引应对多版本问题,只简单实现了一小部分。当自己无法判断处理时,就先取出来,最后还是交给数据行的mvcc机制决定这条数据是否符合。具体实现方式:
- 给每个索引都加一个隐藏字段(最后修改的事务ID)。再加一个索引链,用来挂改之前的老数据。
- 修改索引字段时,不是直接改,而是新增一个索引数据,然后把老数据挂在索引链上。
- 查询的时候,如果原来是判断等于xx = 数据,那么此时的判断逻辑就相当于变成了 xx in(新数据, 老数据1,老数据2)。如果查出来符合,但此时的数据还不能直接返回给用户,还需要根据主键找到具体数据行,最后根据上面mvcc的原理决定是否返回给用户。
具体看博客(mysql二级索引没有mvcc_MySQL InnoDB MVCC深度分析),或者直接看官网(Clustered and Secondary Indexes)。
这里你可能会产生的疑惑:二级索引的判断如何跟聚簇索引保持一致的?(二级索引可以不完全判断一个数据的可见性,但不能跟聚簇索引判断出来的结果矛盾。就像基层员工由于没经验,可以不完全知道怎么处理一件事,但他不能告诉客户一个跟领导完全相悖的答案)
他们能保证不出现自相矛盾的方法,靠的就是数据访问者手里的那个ReadView,这是二级索引和聚簇索引共同使用的判断数据版本的依据。
这里我发现一个很有意思的事情。以前我总觉得看不懂官网是自己英文太差。但实际上,可以看看这篇文章(原封不动翻译的官网文章)。如果不是看了前面那篇博客,这一坨中文同样让人看不懂。
其他
undo log到底是什么
前面我们说版本链的时候,说版本链是有各个版本的数据串起来的链。其实这个说法不准确。这串起来的其实是undo log。并不是数据行。
在学习redo log,undo log的时候,就会强调:redo log是物理日志,undo log是逻辑日志。undo log可以简单理解为sql语句。
通过这样的sql语句 和 新版数据 就可以得到上一个版本的老数据(减少空间浪费)。
快照读,当前读
在学mvcc时,还会“莫名其妙”的提到快照读,当前读这两个词。之所以说莫名其妙,因为粗看这两个词的功能,似乎和mvcc并没有很强的功能上的关联。你可能还会听到一致性非锁定读这个词,更觉得有点晕。
其实快照读就是非锁定读(Nonlocking Reads),当前读就是锁定读(Locking Reads)。我觉得还是直译为锁定读/非锁定读更直观好理解。因为这两个词直接区分的就是是否用锁。
无论是否用锁,我们都希望读到的数据不要受外界(其他写操作)干扰,变来变去。所以就加了个一致性。怎么实现一致性呢? 加锁的很好理解,隔离级别为串行化时,就是通过加锁,读写完全串行化,但性能很差。那不加锁呢?就是通过这里的主角MVCC机制实现。
由于锁定读肯定是一致的,所以在看官网时,目录:一个叫“Consistent Nonlocking Reads”,一个叫“Locking Reads”(把一致性直接省略了)。这也是为什么你听过“一致性非锁定读”这个词,但没见过“一致性锁定读”这种说法。
事实上:mysql官网里,一致读(Consistent Read)就特指MVCC的在RC/RR两种隔离级别的情况。
乐观锁?
有人说MVCC是一种乐观锁的实现。这种说法是不对的。
数据库中的乐观锁指的是两个写操作之间的实现方式。改的时候不加锁,先查版本号(也可根据自己的业务情况,设计其他类型的唯一字段),保存时根据版本号判断是否要保存(CAS机制),如果已经有其他写操作已经把版本号改了,那就保存不了。
select version from table;--得到oldV
update table set col=newData, vesion=(oldV + 1) where version=oldV and 其他条件;
乐观锁是用户自己根据业务场景,自己实现的CAS机制(比如实现在多线程或集群下对资源的抢占。一种简易的分布式锁)。mysql并没有这种乐观锁机制。
普通select
包括官网里,在说普通select的时候,其实指的都是包含在事务里的普通select语句。不要误以为直接操作数据库,写了一句select就是普通select。不加事务的select,查询只能读到提交后的数据,和mvcc机制没有关系。看下面这个图
一致性
这个词很容易让人误解。我觉得根源在于中文里没有一个独立且合适的简单词汇来代表“一贯不变”。一致和一致性 是两个看似一样,但意思却很不同的词,前者单纯的表示“一样”,后者是英文Consistent的翻译(一贯不变)。
在计算机行业里,即便是同样都用一致性,意思也会有细微差别。数据库里的一致性和一致性hash也不太一样。前者指的是一个事物在经过一些列操作之后依然得到正确的结果。后者强调的是多个个体之间(横向),在一段时间里(纵向)表现都一致。这种细微的含义差别,英文里也没有区分,都用的是Consistent。
总结
- MVCC是为实现隔离性的一种无锁方案。
- 相比有锁方案,实现了读写并行处理,提高了系统性能。
- 实现机制是多版本控制。
参考
面试官:谈谈你对Mysql的MVCC的理解?
MySQL中MVCC的正确打开方式(源码佐证)
MySQL事务日志(redo log和undo log)的详细分析
mysql二级索引没有mvcc_MySQL InnoDB MVCC深度分析
Clustered and Secondary Indexes