本博文将描述MVCC和cow技术以及LMDB中如何使用以及实现这两种技术。

COW(Copy On Write):

COW技术背后的思想是拖延技术,基本方法是假如有多个调用者需要访问的资源,在其初始化的时候是不能区分的,即对于多个调用者来说,这资源就是一样的。这样就可以给每个

调用者一个指向资源的指针即可。这种方法一直持续到调用者需要进行修改所需要访问的资源时,在这个时候,调用者将被分配到一份真正私有的资源拷贝,这样调用者对资源的任意

改动对于其它调用者来说都是不可见的。所有的以上操作对于调用者来说是透明的,这种方法最大的好处就是假如调用者不修改资源,私有拷贝就不会被创建。因此这种技术对于读大于

写的应用场景来说特别合适,比如说虚拟内存与分页、数据库、string对象等等,都使用此技术以提升系统性能。

MVCC(Multiversion concurrency control):

MVCC是数据库系统实现并发控制和一般系统中实现事务性内存控制的一种技术,主要用于并发控制,使用MVCC将读不会阻塞写,写不会阻塞读,只有两个线程写同一行数据可能导致冲突,

因此可以提供最高的并发性。MVCC对于并发写同一区域的数据可能导致冲突要求事务回滚或者互相等待资源导致死锁。

对于数据库系统来说,若一个用户正在读数据的同时,另一个用户正在写同一个数据,则读用户可能读到写到一半的数据或者不一致的数据,因此数据库系统都会使用并发控制。最简单的方式就是

写时阻塞所有读,这就是封锁技术。MVCC的方式是每个用户看到的数据是其连接上数据库时的快照,所有写操作对数据的改变其他数据库用户都不能看到,除非改变已经完成(即事务已提交)。

在MVCC中数据的更新使用delete(标记)+insert实现,因此对于同一行数据可能在数据库多个地方存在不同版本,旧数据和新数据不在同一个地方(不是就地更新),但只有最后一个版本是最新的,

之前的版本对于之前的事务有效。MVCC的好处是对于读数据来说,哪怕数据在整个事务过程中被别的用户修改删除,其读到的数据就是刚开始使用数据库时的那份数据。另外就是其不需要及时删除

旧数据,这样避免了系统来回换页导致性能下降。对于文档型数据库来说,还可以优化为数据存储在连续区块,delete+insert可以不更新里面部分数据,这样对于后续组装数据提供最大便利。MVCC

提供了即时的一致性视图,读写隔离,不需要进行封锁,因此可以提高并发性。

MVCC的图示:

图1:事务T1改变数据V1,将其改为数据V2,在堆中,数据如下图

lightning mdb 源代码分析(4)—MVCC/COW_第1张图片

图2:事务T3改变了V2,将其改为V3,在堆中,数据如下图:目前事务T2还在活动中,所以V1和V2属于recently dead状态,而不是真的dead状态。

lightning mdb 源代码分析(4)—MVCC/COW_第2张图片

图3:从可视性而言,事务T0只能看到数据V1。因为它早于事务T1启动。

lightning mdb 源代码分析(4)—MVCC/COW_第3张图片

图4:事务T1提交后,事务T2启动,此时事务T3尚未启动,故T2可以看到T1提交后的数据V2。

lightning mdb 源代码分析(4)—MVCC/COW_第4张图片

图5:事务T3提交后,事务T4启动,故T4只能看到数据V3。

lightning mdb 源代码分析(4)—MVCC/COW_第5张图片

图6: 前面说过,当还有事务活动中访问数据V1和V2,V1和V2的状态是recently dead。

当T0和T2都结束,已经没有事务在访问数据V1和V2了,此时V1和V2为dead状态,所以V1和V2都成为VACUUM的处理对象了。

lightning mdb 源代码分析(4)—MVCC/COW_第6张图片

 

以上几图以postgresql的MVCC实现为例描述了不同事务间读写操作过程以及其访问的数据。对于同时更新来说,主要多了

更新冲突检测,若更新存在读写依赖冲突则,更新失败,事务必须回滚,若存在互相依赖,则会解锁某一个事务,以避免死锁。

 

MVCC/COW在LMDB中的实现

LMDB对MVCC加了一个限制,即只允许一个写线程存在,从根源上避免了写写冲突,当然代价就是写入的并发性能下降。因为只有

一个写线程,所以不会不需要wal日志、读写依赖队列、锁队列等一系列控制并发、事务回滚、数据恢复的基础工具。MVCC的基础

就是COW,对于不同的用户来说,若其在整个操作过程中不进行任何的数据改变,其就使用同一份数据即可,若需要进行改变,比如

增加、删除、修改等,就需要在私有数据版本上进行,修改完成提交之后才给其他事务可见。

LMDB中,数据操作的基本单元是页,因此cow也是以页为单位,对应函数是mdb_page_touch,mdb_page_copy,copy真正实现页面复制,

touch调用copy完成复制,然后修改pgno后插入到B+Tree当中,这样对于此次事务,后续的操作访问的数据页就是最新的数据页面,而非

事务启动时对应的数据页面,且此页面与其他页面的关联关系仅在本事务页面列表中可见,对其他事务不可见。

实际上通过以上两个函数也实现了MVCC的核心,对于读写的控制,通过mdb_txn_begin控制,在其中,事务启动时会检查读写锁的情况,

若事务需要更新数据,则会被阻止,若只是读数据,则不管是否有写事务存在,读锁都可以获得。

MVCC的一个副作用就是对于存在大量写的应用,其数据版本很多,因此旧数据会占用大量空间,postgresql解决此问题通过vacuum命令,

LMDB中通过freedb解决,即将不再使用的旧的数据页面空间插入到一颗b-Tree当中,这样旧空间在所有事务不再访问之后就可以被LMDB

使用,从而避免了需要定期执行清理操作。当然其副作用是数据只能保持最新不能恢复到任意时刻,未执行vacuum之前,保存所有版本的数据库

可以恢复到任意时刻。

 

本文参考了如下资料,在此一并表示感谢。

https://en.wikipedia.org/wiki/Multiversion_concurrency_control

http://www.kuqin.com/system-analysis/20120319/319108.html

http://blog.chinaunix.net/uid-20726500-id-4040024.html

http://www.cnblogs.com/gaojian/p/3295951.html