Multiversion Concurrency Control
简单理解:
用于多事务并发环境下,对于数据读写在不加锁的情况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为Read Commit和Repeatable Read中使用到(mysql数据库)。
最大优势:读不加锁,读写不冲突。
MVCC没有一个统一的实现标准,各种数据库系统都实现了MVCC,但实现机制各不相同。
可以认为它是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
非常重要的表述!反复理解——
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面是在RR隔离级别下,MVCC具体如何操作——
SELECT。InnoDB会根据以下两个条件检查每行记录。
- a. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- b. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
只有符合以上a、b两个条件的记录,才能返回作为查询结果。
- INSERT。InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
- DELETE。InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
- UPDATE。InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
只看规则理解会有些困难,我们以简单的例子来说明分析。
这里提醒一个可能会产生困惑的点:
每次开启一个事务,系统版本号都会递增(这个系统版本号也就是事务的版本号),这和行版本号没有关系!数据表中数据的行版本号是不变的!
把这个规则理解为所有行的创建时间版本号会自动递增是绝对错误的!!!
首先,本来系统版本号是0,user表里没数据。我们创建一个事务T1,当前系统版本号加1,变成1,这也就成了事务T1的版本号。这个事务T1向表中insert了3条数据,因此3条数据行的创建时间(行的系统版本号)都被保存为当前的系统版本号,即1。当前表3-1。
表3-1:
id | name | DB_TRX_ID(行的创建版本号) | DB_ROLL_PT(行的删除版本号) |
---|---|---|---|
1 | X | 1 | undefined |
2 | Y | 1 | undefined |
3 | Z | 1 | undefined |
然后,我开启一个事务T2,它的版本号是2;然后开启一个事务T3,它的版本号是3
假定以上事务T2、T3都只是select查询,所以不涉及对行的版本号的任何修改!
也就是说此时它们所检查的数据库表,行的版本号都仍然是1!
所以目前的数据表跟上面的表内容相同。
再然后,我开启一个事务T4,当前系统版本号变成4,事务T4的版本号也是4了。而它当中包含了一条update语句:
begin;
update user set name = 'P' where id = 2;
commit;
该语句想要将id为2的行记录,name修改为P。根据上面提到的UPDATE语句的规则,InnoDB会插入一行新记录,并且将该新记录的行版本号设置为当前的系统版本号4,同时将旧行的删除版本号设置为当前的系统版本号4。
于是,数据库user表变成了这样:
表3-2
id | name | DB_TRX_ID(行的创建版本号) | DB_ROLL_PT(行的删除版本号) |
---|---|---|---|
1 | X | 1 | undefined |
2 | Y | 1 | 4 |
2 | P | 4 | undefined |
3 | Z | 1 | undefined |
我们再来细究一下事务T4的发展过程,说明为什么这样可以达成我们需要的并行效果:
我们假设T3事务的SELECT查询是这样的。
begin;
select * from user; # 记为查询1
select * from user; # 记为查询2
commit;
事务T3、T4是有先后顺序的。如果事务T3完成之后事务T4才执行的话,那没有任何分析的意义。我们考虑的就是并行冲突,
所以T4是在T3执行过程中发生甚至完成的。我们按照这个思路走一遍流程——
- 事务T3先开始,执行了查询1,此时它检查的数据表是表3-1那个样子。查出来的结果自然是包含3条记录(X、Y、Z);
- 此时事务T3的查询2还没有开始执行,事务T4已经开始并提前完成了提交。这意味着当事务T3的查询2开始执行时,实际上检查的数据表已经变成了表3-2那个样子。
它去查数据表3-2时,会依据上面提到的SELECT语句规则,拿着事务T3的版本号3,去比对每行数据的创建版本号和删除版本号——
- id=2且name=Y的那条数据(也就是原数据),它的行创建版本号仍是1,小于当前事务T3的版本号3,满足条件a,同时它的行删除版本号是4,大于当前事务版本号3,同样满足条件b。同时满足a、b条件,因此可以作为查询结果。
- id=2且name=P的那条数据,它的行创建版本号4大于了事务版本号3,不满足条件a,所以不作为查询结果。
因此,事务T3的查询2返回的结果,和查询1返回的结果相同。
这就实现了开始时间不同的不同事务,对同一张表,同一时刻看到的数据可能不一样这个效果。
通过保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作简单,性能很好,并且也能保证只会读到符合标准的行。不足之处就是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在RR和RC两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容。因为RU总是读取最新的数据行,而不是符合当前事务版本的数据行。而S则会对所有读取的行都加锁。