通过本文将弄明白以下几个问题。
一、思考
问题一:MVCC是什么?主要为了解决什么问题?
问题二:MVCC的实现原理是什么?
问题三:有了MVCC之后,开发者还需要做什么?
二、分析
2.1概念
该定义由此博文总结:https://draveness.me/database-concurrency-control。
MVCC,即多版本并发控制,在这个机制中,每一个写操作都会创建一个新版本的数据,每一个读操作将从有限多个版本的数据中选择一个最合适的返回,而管理和快速挑选数据的版本就是MVCC的工作。
其主要目的在于解决数据库的“并发读写”能力。
2.2举例
在博文【MYSQL---锁】中,我们提到在更新数据时,InnoDB引擎会默认为数据加X锁,此时其它事务不能对该数据行加X锁和S锁,但是能读取该数据,这便提高了数据库的“并发读写”能力,如下:
假设有表t,字段(id,name);事务A和B。初始数据(1,'a');事务隔离级别为RR。
事务A | 事务B |
SET autocommit=0; BEGIN; | SET autocommit=0; BEGIN; |
//步骤一(获取X锁) UPDATE t SET name='b' WHERE id=1; //name=b | |
//步骤二(依然可读,读写并发,MVCC控制) SELECT name FROM t WHERE id=1; // name=1; | |
..... COMMIT; | ..... COMMIT; |
由上图可知,事务B虽然拥有了数据行的X锁,事务A虽然不能再加X锁和S锁,但是仍然可读数据(这是RR隔离级别的特性,所以说可以说MVCC可以控制采用RR或者RC,请看后文分析),而且读到的数据是事务开始时的数据,这便解决了“并发读写”问题,那MVCC到底是怎么做的呢,请看下文分析。
2.3MVCC实现原理
要解决上述所说的“并发读写问题”,那么事务A读到的数据必然和事务B修改的数据不是同一份数据,在MVCC中,可以称为不是同一版本的数据。那么就有两个问题需要我们思考:
1.如何构建多版本?
2.事务A如何知道选择哪个版本数据?
在看下文之前,您可以看一下这篇文章:https://juejin.im/post/5c68a4056fb9a049e063e0ab,其通俗易懂的解释了多版本并发控制(MVCC)中的增删查改操作。
InnoDB默认会给每行加三个字段
InndoDB在创建表时,会默认为表增加三个字段,其中有两个字段是与MVCC相关的:
DB_TRX_ID(下文中以tx_id表示):数据行的版本号,表示该条数据最后被修改时的事务id。
DB_ROLL_PT:删除版本号,表示该条数据被删除时的事务id。
快照
首先在事务系统中,会维护一个全局的活跃事务id(descriptors)。
当要创建一个快照(readview)会将上述的全局的活跃事务id拷贝一份到新建的快照(数据结构见下文)。当事务内根据条件查询某条数据时,可能会查询到数据的多个版本,这时对于查询出来的每一条数据,都会根据快照判断其是否满足可见性(可以被当前事务看见),如果可以则返回,否则就利用undolog来构建历史版本数据,直到构建到最老的版本或者可见性满足。
所以快照的主要作用就是“数据可见性判断”。
快照的数据结构(模拟):
readview{
int [] descriptors:数组,存储数据库中所有活跃事务(已经开始,但未提交;不包含只读事务)的事务id,id从小到大排序。
int up_limit_id:descriptors数组中最小的值。
int low_limit_id:创建快照时产生的最大事务id(max_trx_id),该值一定大于descriptors数组中的最大事务id。
}
数据可见性判断
有了前面介绍的两个知识点,我们现在来分析一下,MVCC是如何判断数据是否可见的。
其会用当前数据行的tx_id和快照中的up_limit_id和low_limit_id进行比较:
- 如果tx_id<up_limit_id:表示这条数据最后修改在快照被创建之前,因此可见。
- 如果tx_id>=low_limit_id,表示这条数据最后修改在快照被创建之后,因此不可见。
- 如果up_limit_id<=tx_id<low_limit_id,表示这条数据在快照创建之时,由其它活跃事务修改,因此不可见。
- 如果tx_id不在descriptors数组之中,经过前面的判断,这种情况可能存在于 descriptors数组中最大值<tx_id<low_limit_id,因为有了第三条,所以这里仍然表示这条数据最后修改在快照被创建之前,因此可见。或者descriptors数组为空,不能存在活跃性事务,那说明修改这条数据的事务已经全部提交,因此可见。
经过以上的判断,如果仍然未找到可见性数据,则通过undolog去构建老版本数据直到找到可以被看见的数据或者undolog被解析完毕。
可重复读、读已提交与MVCC关系
其实可重复读和读已提交的根本不同在于数据可见性,“可重复读”在事务提交之后,数据仍然不能被其他事务可见。而“读已提交”,在事务提交之后,数据就能被其他事务看见。这里根本原因是因为MVCC实现中,创建快照的时机不同:
可重复读(RR):快照在第一次查询的时候创建,这个快照会一直持续到事务结束,期间数据可见性不会改变,所以在当前事务内不会产生数据不一致情况。
读已提交(RC):事务中的每个查询都会创建一个快照,这样如果两个查询之间,有其他事务修改了数据,就导致数据可见性改变,就会产生数据不一致情况,所以不可重复读。
通过MVCC再分析前面案例
上文案例如下:
背景:
假设有表t,字段(id,name);事务A和B。初始数据(1,'a');事务隔离级别为RR。全局活跃事务id(descriptos)中为空,此时该行数据情况如下:
id name tx_id DB_ROLL_PT 1 a 1(前面已经有事务id为1的事务修改了该数据,但是已经提交) NULL
过程:
事务A和事务B执行步骤如下,假设事务A的id为3,事务B的id为2,即事务B先与事务A执行:
事务A 事务B SET autocommit=0;
BEGIN;
SET autocommit=0;
BEGIN;
//步骤一(获取X锁)
UPDATE t SET name='b' WHERE id=1; //name=b
//步骤二(依然可读,读写并发,MVCC控制)
SELECT name FROM t WHERE id=1; // name=1;
.....
COMMIT;
.....
COMMIT;
分析:
1.当事务B执行步骤一之后:
该数据行情况:
全局活跃事务id(descripotors数组)=[2],因为事务B还未提交。
id name tx_id DB_ROLL_PT 1 a 1 2(代表事务B删除了数据) 1 b 2(事务B修改了数据) NULL 2.事务A执行步骤二:
事务A根据id=1查询这条数据,先创建快照如下:
readview{
descripotors:[2,3],即事务A和事务B。
up_limit_id:2,descripotors数组中最小值。
low_limit_id:3,当前事务A的事务id。
}
根据id=1查询到上述两个版本数据,遍历这两条数据,按照上面的“可见性分析”规则,结果为:
第一条数据满足第一条:tx_id<up_limit_id,所以这条数据满足可见性,返回。
通过上面的分析相信你已经可以解答开篇的几个问题,其实弄懂原理之后,并不难。MVCC分析至此结束,如有不对之处,请指正。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
参考文章:
- 五分钟搞懂MVCC机制:https://juejin.im/post/5c68a4056fb9a049e063e0ab。
- MVCC多版本并发控制:https://segmentfault.com/a/1190000012650596。
- 淘宝数据库内核月报-InnoDB事务系统-MVCC:http://mysql.taobao.org/monthly/2017/12/01/。