MVCC多版本并发控制
2018-07-27 17:07:25 阿炯

本站赞助商链接,请多关照。 数据库基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制MVCC。不仅是MySQL,包括Oracle, PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制所有不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

到目前为止我们介绍的并发控制机制其实都是通过延迟或者终止相应的事务来解决事务之间的竞争条件Race condition来保证事务的可串行化;虽然前面的两种并发控制机制确实能够从根本上解决并发事务的可串行化的问题,但是在实际环境中数据库的事务大都是只读的,读请求是写请求的很多倍,如果写请求和读请求之前没有并发控制机制,那么最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的。



在这种大前提下,数据库系统引入了另一种并发控制机制 - 多版本并发控制Multiversion Concurrency Control,每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。

MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现;但是由于它们分别实现了悲观锁和乐观锁,所以 MVCC 实现的方式也不同。

MVCC的实现

其实现是通过保存数据在某个时间点的快照来实现的。即,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻开到的数据可能是不一样的。并发控制大致可分为如下两类:乐观optimistic并发控制、悲观pessimistic并发控制。

乐观锁和 MVCC 的区别

当多个用户/进程/线程同时对数据库进行操作时,会出现3种冲突情形:
读-读,不存在任何问题
读-写,有隔离性问题,可能遇到脏读会读到未提交的数据 ,幻影读等
写-写,可能丢失更新

要解决冲突,一种办法是是锁,即基于锁的并发控制,比如2PL,这种方式开销比较高,而且无法避免死锁。

MVCC
多版本并发控制MVCC是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前(看到)的数据库的快照。这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。

OCC
乐观并发控制OCC是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。乐观并发控制类似自选锁。乐观并发控制适用于低数据争用,写冲突比较少的环境。

多版本并发控制可以结合基于锁的并发控制来解决写-写冲突,即MVCC+2PL,也可以结合乐观并发控制来解决写-写冲突。每种存储引擎实现MCVV的方式是不同的,例如乐观并发控制,悲观并发控制。


下面通过InnoDB的简化版行为来说明MVCC是如何工作的。

InnoDB的MVCC,是通过在每行纪录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间,存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行纪录的版本号进行比较。

也可以这么来理解:
InnoDB通过为每个数据行增加两个隐含值的方式来实现MVCC,这两个隐含值记录了行的创建时间,以及它的过期时间或者叫删除时间,每一行都存储了事件发生时的系统版本号,用来替代事件发生时的实际事件。关于系统版本号我们不用去追求,可以看作行数据的唯一标识符。每一次,开始一个新事务时,版本号都会自动递增。每个事务都会保存它在开始时的“当前系统版本”的记录,而每个查询都会根据事务的版本号,检查每行数据的版本号。

MySQL 与 MVCC

MySQL 中实现的多版本两阶段锁协议Multiversion 2PL将 MVCC 和 2PL 的优点结合了起来,每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。


更新操作就稍微有些复杂了,事务会先读取最新版本的数据计算出数据更新后的结果,然后创建一个新版本的数据,新数据的时间戳是目前数据行的最大版本 +1:


数据版本的删除也是根据时间戳来选择的,MySQL 会将版本最低的数据定时从数据库中清除以保证不会出现大量的遗留内容。

在REPEATABLE READ可重复读隔离级别下,MVCC具体的操作如下:

SELECT:InnoDB会根据以下两个条件检查每行纪录:
1、InnoDB只查找版本早于当前事务版本的数据行,即,行的系统版本号小于或等于事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。

2、行的删除版本,要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被删除。

只有符合上述两个条件的纪录,才能作为查询结果返回。

INSERT:InnoDB为插入的每一行保存当前系统版本号作为行版本号,行的更新版本被修改为该事务的事务号。

DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识,相当于标记为删除,而不是实际删除。

UPDATE:InnoDB为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。为每个需要更新的行,建立一个新的行拷贝,记录当前的系统版本号,同时也为更新前的旧行,记录系统的版本号,作为旧行的删除版本标识。

优点:保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好。

缺点:每行纪录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

保存这些额外记录的好处是使大多数读操作都不必申请加锁,这使都操作变得尽可能的快,因为读操作只要选取符合标准的行数据即可。这种方式的缺点是,存储引擎必须为每行数据存储更多的额外数据,做更多的行检查工作,以及处理一些额外的整理操作。

INNODB支持的事务隔离等级
 
INNODB支持并实现了ISO标准的4个事务隔离等级,即 READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE。
 
1) READ UNCOMMITTED (可以读未提交的): 查询可以读取到其他事务正在修改的数据,即使其他事务的修改还没有提交.这种隔离等级无法避免脏读.
 
2) READ COMMITTED(只可以读已经提交的):其他事务对数据库的修改,只要已经提交,其修改的结果就是可见的,与这两个事务开始的先后顺序无关.这种隔离等级避免了脏读,但是无法实现可重复读,甚至有可能产生幻读.
 
3) REPEATABLE READ(可重复读):比read committed更进了一步,它只能读取在它开始之前已经提交的事务对数据库的修改,在它开始以后,所有其他事务对数据库的修改对它来说均不可见.从而实现了可重复读,但是仍有可能幻读
 
4) SERIALIZABLE(可串行化):这是事务隔离等级的最高级别.其实现原理就是对于所有的query,即使是查询,也会加上读锁,避免其他事务对数据的修改.所以它成功的避免了幻读.但是代价是,数据库系统的并发处理能力大大降低,所以它不会被用到生产系统中.

不同事务隔离等级下的MVCC实现

MVCC由于其实现原理,只支持read committed和repeatable read隔离等级。READ UNCOMMITTED隔离级别不兼容MVCC,因为在任何情况下,该隔离级别下的查询,不读取符合当前事务版本的数据行,而读取最新版本的数据行,SERIALIZABLE隔离级别也不兼容MVCC,因为该级别下的读操作会对每一个返回行都进行加锁。


PostgreSQL 与 MVCC

与 MySQL 中使用悲观并发控制不同,PostgreSQL 中都是使用乐观并发控制的,这也就导致了 MVCC 在于乐观锁结合时的实现上有一些不同,最终实现的叫做多版本时间戳排序协议Multiversion Timestamp Ordering,在这个协议中,所有的的事务在执行之前都会被分配一个唯一的时间戳,每一个数据项都有读写两个时间戳:


当 PostgreSQL 的事务发出了一个读请求,数据库直接将最新版本的数据返回,不会被任何操作阻塞,而写操作在执行时,事务的时间戳一定要大或者等于数据行的读时间戳,否则就会被回滚。

这种 MVCC 的实现保证了读事务永远都不会失败并且不需要等待锁的释放,对于读请求远远多于写请求的应用程序,乐观锁加 MVCC 对数据库的性能有着非常大的提升;虽然这种协议能够针对一些实际情况做出一些明显的性能提升,但是也会导致两个问题,一个是每一次读操作都会更新读时间戳造成两次的磁盘写入,第二是事务之间的冲突是通过回滚解决的,所以如果冲突的可能性非常高或者回滚代价巨大,数据库的读写性能还不如使用传统的锁等待方式。


总结

为了实现可串行化,同时避免锁机制存在的各种问题,我们可以采用基于多版本并发控制Multiversion concurrency control,MVCC思想的无锁事务机制。人们一般把基于锁的并发控制机制称成为悲观机制,而把MVCC机制称为乐观机制。这是因为锁机制是一种预防性的,读会阻塞写,写也会阻塞读,当锁定粒度较大,时间较长时并发性能就不会太好;而MVCC是一种后验性的,读不阻塞写,写也不阻塞读,等到提交的时候才检验是否有冲突,由于没有锁,所以读写不会相互阻塞,从而大大提升了并发性能。我们可以借用源代码版本控制来理解MVCC,每个人都可以自由地阅读和修改本地的代码,相互之间不会阻塞,只在提交的时候版本控制器会检查冲突,并提示合并。

一般我们认为MVCC有下面几个特点:
每行数据都存在一个版本,每次数据更新时都更新该版本
修改时Copy出当前版本, 然后随意修改,各个事务之间无干扰
保存时比较版本号,如果成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,因为这看起来正是在提交的时候才能知道到底能否提交成功

参考来源:

MySQL--多版本并发控制

深入理解MVCC多版本并发控制


浅谈数据库并发控制 - 锁和 MVCC

MySQL-InnoDB-MVCC多版本并发控制