本博文系列前面已经探讨了LMDB的系统架构、MMAP映射、B-Tree操做等部分,本文将尝试描述LMDB中的事务控制的实现。数据库
事务的基本特征:数据结构
事务是恢复和并发控制的基本单位。它是一个操做序列,这些操做要么都执行,要么都不执行,它是一个不可分割的工做单位。多线程
事务是数据库维护数据一致性的单位,在每一个事务结束时,都能保持数据一致性。架构
事务应该具备4个属性:原子性、一致性、隔离性、持久性。这四个属性一般称为ACID特性。并发
原子性(atomicity)。一个事务是一个不可分割的工做单位,事务中包括的诸操做要么都作,要么都不作。atom
一致性(consistency)。事务必须是使数据库从一个一致性状态变到另外一个一致性状态。一致性与原子性是密切相关的。线程
隔离性(isolation)。一个事务的执行不能被其余事务干扰。即一个事务内部的操做及使用的数据对并发的其余事务是隔离的,并发执行的各个事务之间不能互相干扰。对象
持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其余操做或故障不该该对其有任何影响。接口
LMDB中的实现基本思路:队列
Atom(A):LMDB中经过txn数据结构和cursor数据结构的控制,经过将脏页列表放入dirtylist中,当txn进行提交时再一次性统一刷新到磁盘
中或者abort时都不提交保证事务要不全成功、要不全失败。对于长事务,若页面spill到磁盘,由于COW技术,这些页面未与整棵B-Tree的root
page产生关联,所以后续的事务仍是不能访问到这些页面,一样保证了事务的原子性。
其数据就是一致的,不存在由于多线程同时写数据致使数据产生错误的状况。
Isolation(I):事务隔离经过锁控制(MUTEX),LMDB支持的锁互斥是进程级别/线程级别,支持的隔离方式为锁表支持,读读之间不锁,写等待读完成以后开始,
读等待写完成后开始
Duration(D):LMDB中,没有使用WAL、undo/redo log等技术来保证系统崩溃时数据库的可用性,其保证数据持续可用的技术是COW技术和只有一线程写技术。
假如LMDB或者系统崩溃时,只有读操做,那么数据原本就没有发生变化,所以数据将不可能遭到破坏。假如崩溃时,有一个线程在进行写操做,则只须要判断最后的
页面号与成功提交到数据库中的页面号是否一致,若不一致则说明写操做没有完成,则最后一个事务写失败,数据在最后一个成功的页面前的是正确的,后续的属于
崩溃事务的,不能用,这样就保证了数据只要序列化到磁盘则必定可用,要不其就是尚未遵循ACI原则序列化到磁盘
顺便说一句,由于MMAP技术、只一个写线程的实现方案,因此数据库进行备份时特别简单,只要按期在线热备整个数据库便可完成。同时恢复也将比较快。固然因为
其使用了重用旧页技术,LMDB在恢复时只能恢复到最新状态,不能恢复到任意时刻。
实现方法:
LMDB支持嵌套事务,不指望在子事务完成以前父事务有任何读写操做,这样的话能够避免父子事务之间的数据不一致。
LMDB不支持跨线程事务,一个事务只能属于一个线程,一个线程在任一时刻只能持有一个事务。
mdb_txn_begin:
开启一个事务,根据是否传入父事务判断是否为子事务,根据传入参数判断是否为只读事务。嵌套事务支持只支持一个子事务,且子事务为写事务父事务也必须为写事务,
并且数据库不能为mmap可写方式。事务开启流程:分配内存,设置变量,若为子事务,设置父子相关关联变量并shadow父亲全部cursor以减小IO读取。不然调用renew0完成
最终的事务开启工做。
mdb_txn_abort:
放弃一个事务,有子事务则先放弃子事务,而后调用reset0真正执行结束操做。
mdb_txn_commit:
提交一个事务,有子事务则先提交子事务,若为只读事务,则关闭全部打开的数据库句柄并保持打开状态,而后放弃事务便可,若为可写事务,肯定事务状态是否正确,若为error
状态,不能够提交,若不是则根据是否存在父事务进行处理,没有父事务则首先更新数据库的root节点,而后保存可重用空间到freedb以便空间重用,并释放midl空间以后,进行
页面刷新,同步相关环境变量以后释放内存,最后释放写锁,至此没有父事务状况提交完成。如有父事务,则其进行将midl列表与父事务的midl合并,cursor一样合并到父事务中进行
最终关闭,将dirtylist合并到父事务中,相关合并和本事务的变量内存释放完毕以后,子事务提交成功,即子事务主要完成内存释放,其余动做如磁盘刷新等都合并至父事务中一次性完成。
mdb_txn_reset:
放弃一个事务,可是保留句柄,仅对只读事务有效,一样调用reset0进行真正事务结束操做。
mdb_txn_reset0 :
放弃事务的公共代码.首先关闭事务中打开的数据库句柄。如果只读事务,设置事务相关变量便可,若为可写事务,须要关闭全部游标,而后释放midl空间,最后释放写锁。至此事务
关闭完毕。
mdb_txn_renew:
重用一个只读事务句柄,避免一次内存分配,检查是否有严重错误,如有失败,没有的话调用renew0完成。
mdb_txn_renew0:
renew0是renew和begin的公共代码。如果写事务,申请进程间互斥锁,如果读事务,首先检查本线程是否已经有读事务,有不支持返回错误,没有的话,开始申请读表互斥锁,
成功后将线程id记录到读表里面,而后马上释放读表锁。而后再次确认线程中确有事务。事务(读写)申请成功后,将env的meta页面根据txnid进行切换,轮流使用。
最后再次设定些变量后通知调用者申请成功。
mdb_txn_env:
返回事务关联的env对象
上文解释了LMDB实现事务控制的方式和主要接口方法的基本流程,若实现相似关系型数据库的细粒度事务,则须要更细粒度的锁以及复杂的页面等待队列机制等以保证行锁或表锁
的正确性并最终实现事务控制机制,且在数据库应用时有可能陷入死锁状态,而在LMDB当中,读写锁分开,且进程崩溃时,系统会释放相关内核变量,从而保证要不进程正常,
锁成功释放,要不进程崩溃,系统释放锁,所以数据库永远不会陷入死锁状态,不过若事务在等待写锁,有可能等待较长时间。
但愿各位能积极批评指正以及转载。