前言
Multiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.[1]
http://en.wikipedia.org/wiki/Multiversion_concurrency_control
个人理解,是数据库在实现事务隔离等特性下,尽量不减少并发处理能力的一种解决方案。
为了更好说明MVCC的作用,我们最好先复习下事务隔离级别都是哪些:
Serializable、Repeatable reads、Read committed、Read uncommitted
维基百科解释很拗口,很多没有接触过数据库系统的同学更不是很理解,建议大家可以看下如下MySQL中各个事务隔离级别各个客户端的表现,就很明白了,http://www.cnblogs.com/zemliu/archive/2012/06/17/2552301.html
简单总结如下:
Serialzable是最强的事务隔离级别,对同一数据的更新必须是串行化的,任何写入操作都将建立写入锁,其他并发提交的事务都将会失败,一般实现这个事务隔离级别对性能影响都非常巨大,故应用场景比较有限。
Repeatable read是MySQL默认事务隔离级别,对于任何一个打开的客户端事务,其所示的数据都应是静态的,当然自己更新的数据除外,字面意思可以可以放心的重复读其他事务可能更新的数据,这些数据对于我来言是静态的,当然自己客户端更新的操作除外。
Read committed 是只仅能读到被客户端提交的数据,也可以说只要提交过的数据其他客户即可见。
Read uncommitted 是可以读到客户端未提交的数据,比如客户端a打开一个事务,做了些更新,但是没有提交,但是客户端b却可以看见a更新的数据,也就说数据库不做任何干预的话,应该是这种事务隔离级别。
事务隔离级别从上往下依次减弱,数据库实现的成本也越来越小,可支持的并发相应也越来越大。
正文一
- MySQL innodb MVCC实现
innodb MVCC主要是为Repeatable-Read事务隔离级别做的。在此隔离级别下,A、B客户端所示的数据相互隔离,互相更新不可见,MySQL是如何做到的呢?
首先来看下innodb的行结构、Read-View的结构,这个对于理解innodb mvcc的实现由重要意义:
我们看到的innodb row中包含: DATA_TRX_ID,DATA_ROLL_PTR,DELETE BIT, 6字节的DATA_TRX_ID标记了最新更新这条行记录的transaction id,7字节的DATA_ROLL_PTR指向当前记录项的undo信息,找之前版本的数据就是通过这个指针,DELETE BIT位用于标识该记录是否被删除。
引自@何_登成 http://hedengcheng.com/?p=148 推荐大家阅读
Read-view数据结构
我看的MySQL版本时5.5.32,其他版本略有不同
storage/innobase/include/read0read.h 如下
及storage/innobase/read/read0read.c read_view_open_now()
storage/innobase/include/read0read.ic read_view_sees_trx_id()
- innodb可见性判断
到这里我们也就不难看出实际实现就是这两个数据结构进行比较:
InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view)在read_vied_sees_trx_id方法里我们有如下比较:
low_limit_id是“高水位”即当时活跃事务的最大id,如果读到row的db_tx_id>=low_limit_id,说明这些id在此之前的数据都没有提交,如注释中的描述,这些数据都不可见。
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
up_limit_id是“低水位”即当时活跃事务列表的最小事务id,如果row的db_tx_id<up_limit_id,说明这些数据在事务创建的id时都已经提交,如注释中的描述,这些数据均可见。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
在两个limit_id之间的我们需要从小到大逐个比较一下:
n_ids = view->n_trx_ids;
for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids – i – 1);if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}
这样我们在要在事务中获取100行数据,我们就能根据这100行的row db_tx_id即本事务的read_view来判断此版本的数据在事务中是否可见。
如果数据不可见我们需要去哪里找上版本的数据呢?就是通过刚才提到过的7BIT的DATA_ROLL_PTR去undo信息中寻找,同时再判断下这个版本的数据是否可见,以此类推。
- innodb更新
更新这个大家一般都比较熟悉,我这里简单表述一下,如一个测试表:
create table test (key int primary key,value varchar(10));
insert
InnoDB为每个新增行记录,如insert into test value(’1′,’aaa’), 会创建新的row,row db_tx_id即为当前系统版本号作为创建ID。
update
如update test set value=’bbb’ where key =’bbb’,InnoDB会复制了一行,这个新行的版本号使用了本次db_tx_id更新的版本号。它也把之前版本号作为了删除行的版本,即把原有row delete bit置为删除,不可见。
delete
InnoDB为每个删除行的记录当前系统版本号作为行的删除ID,也就是说把之前说的BIT位置成不可见的。
我这里主要描述的是Innodb主键更新,二级索引的更新大多会映射或转换为多条的主键更新,之前提到@何_登成 博客http://hedengcheng.com/?p=148 中举了几个主键更新和二级索引更新的例子,大家可以参看下。
后续也会针对Hbase,PostgreSQL,Oracle的MVCC进行对比和分析,来看下其他数据库的实现方式。
参考
http://en.wikipedia.org/wiki/Multiversion_concurrency_control
http://en.wikipedia.org/wiki/Isolation_%28database_systems%29
http://blog.csdn.net/chen77716/article/details/6742128
http://www.cnblogs.com/zemliu/archive/2012/06/17/2552301.html