做为一款优秀的非内存数据库,HBase和传统数据库同样提供了事务的概念,只是HBase的事务是行级事务,能够保证行级数据的原子性、一致性、隔离性以及持久性,即一般所说的ACID特性。为了实现事务特性,HBase采用了各类并发控制策略,包括各类锁机制、MVCC机制等。本文首先介绍HBase的两种基于锁实现的同步机制,再分别详细介绍行锁的实现以及各类读写锁的应用场景,最后重点介绍MVCC机制的实现策略。算法
HBase提供了两种同步机制,一种是基于CountDownLatch实现的互斥锁,常见的使用场景是行数据更新时所持的行锁。另外一种是基于ReentrantReadWriteLock实现的读写锁,该锁能够给临界资源加上read-lock或者write-lock。其中read-lock容许并发的读取操做,而write-lock是彻底的互斥操做。数据库
Java中,CountDownLatch是一个同步辅助类,在完成一组其余线程执行的操做以前,它容许一个或多个线程阻塞等待。CountDownLatch使用给定的计数初始化,核心的两个方法是countDown()和await(),前者能够实现给定计数倒数一次,后者是等待计数倒数到0,若是没有到达0,就一直阻塞等待。结合线程安全的map容器,基于test-and-set机制,CountDownLatch能够实现基本的互斥锁,原理以下:缓存
1. 初始化:CountDownLatch初始化计数为1安全
2. test过程:线程首先将临界资源做为key,latch做为value尝试插入线程安全的map中。若是返回失败,表示其余线程已经持有了该锁,调用await方法阻塞到该latch上,等待其余线程释放锁;数据结构
3. set过程:若是返回成功,就表示已经持有该锁,其余线程必然插入失败。持有该锁以后执行各类操做,执行完成以后释放锁,释放锁首先将map中对应的KeyValue移除,再调用latch的countDown方法,该方法会将计数减1,变为0以后就会唤醒其余阻塞线程。并发
读写锁分为读锁、写锁,和互斥锁相比能够提供更高的并行性。读锁容许多个线程同时以读模式占有锁资源,而写锁只能由一个线程以写模式占有。若是读写锁是写加锁状态,在锁释放以前,全部试图对该锁占有的线程都会被阻塞;若是是读加锁状态,全部其余对该锁的读请求都会并行执行,可是写请求会被阻塞。显而易见,读写锁适合于读多写少的场景,也由于读锁能够共享,写锁只能某个线程独占,读写锁也被称为共享-独占锁,即常常见到的S锁和X锁。性能
Java中,ReentrantReadWriteLock是读写锁的实现类,该类中有两个方法readLock()和writeLock()分别用来获取读锁和写锁。线程
HBase采用行锁实现更新的原子性,要么所有更新成功,要么失败。全部对HBase行级数据的更新操做,都须要首先获取该行的行锁,而且在更新完成以后释放,等待其余线程获取。所以,HBase中对同一行数据的更新操做都是串行操做。3d
如上图所示,HBase中行锁相关的主要结构有RowLock和RowLockContext两个类,其中RowLockContext类存储行锁相关上下文信息,包括持锁线程、被锁对象以及能够实现互斥锁的CountDownLatch对象等等,RowLockContext是RowLock的一个属性,除此以外,RowLock还包含表征行锁是否已经释放的release字段。具体字段以下图所示:对象
1. 首先使用rowkey以及自身线程对象生成行锁上下文RowLockContext对象
2. 再将rowkey做为key,RowLockContext对象做为value调用putIfAbsert方法写入全局map中。key的惟一性,保证map中最多只有一个RowLockContext。putIfAbsent方法会返回一个existingContext对象,该对象表示key插入前map中对应该key的value值,根据existingContext是否为null、是不是自身线程建立,能够分为以下三种状况:
(1)existingContext对象为null,表示该行锁没有被其余线程持有,能够根据建立的上下文对象持有该锁
(2)existingContext是自身线程建立,表示自身线程已经再建立RowLockContext对象,直接使用存在的RowLockContext对象持有该锁。这种状况会出如今批量更新线程中,一次批量更新可能前先后后对某一行数据更新屡次,须要屡次持有该行数据的行锁,在HBase中是被容许的。
(3)existingContext是其余线程建立,则该线程会阻塞在此上下文所持锁上,直至所持行锁被释放或者阻塞超时。若是所持行锁释放,该线程会从新竞争写全局map,一旦竞争成功就持有该行锁,不然继续阻塞。而若是阻塞超时,就会抛出异常,不会再去竞争该锁。
在线程更新完成操做以后,必须在finnally方法中执行行锁释放操做,即调用rowLock.release()方法,该方法主要执行以下两个操做:
1. 从lockedRows这个全局map中将该row对应的RowLockContext移除
2. 调用latch.countDown()方法,唤醒其余阻塞在await上等待该行锁的线程
HBase中除了使用互斥锁实现行级数据的一致性以外,也使用读写锁实现store级别操做以及region级别操做的并发控制。好比:
1. Region更新读写锁:HBase在执行数据更新操做以前都会加一把Region级别的读锁(共享锁),全部更新操做线程之间不会相互阻塞;然而,HBase在将memstore数据落盘时会加一把Region级别的写锁(独占锁)。所以,在memstore数据落盘时,数据更新操做线程(Put操做、Append操做、Delete操做)都会阻塞等待至该写锁释放。
2. Region Close保护锁:HBase在执行close操做以及split操做时会首先加一把Region级别的写锁(独占锁),阻塞对region的其余操做,好比compact操做、flush操做以及其余更新操做,这些操做都会持有一把读锁(共享锁)
3. Store snapshot保护锁:HBase在执行flush memstore的过程当中首先会基于memstore作snapshot,这个阶段会加一把store级别的写锁(独占锁),用以阻塞其余线程对该memstore的各类更新操做;清除snapshot时也相同,会加一把写锁阻塞其余对该memstore的更新操做。
如上文所述,HBase分别提供了行锁和读写锁来实现行级数据、Store级别以及Region级别的并发控制。除此以外,HBase还提供了MVCC机制实现数据的读写并发控制。MVCC,即多版本并发控制技术,它使得事务引擎再也不单纯地使用行锁实现数据读写的并发控制,取而代之的是,把行锁与行的多个版本结合起来,通过简单的算法就能够实现非锁定读,进而大大的提升系统的并发性能。HBase正是使用行锁 + MVCC保证高效的并发读写以及读写数据一致性。
在了解HBase如何实现MVCC以前,咱们首先须要了解当前仅基于行锁实现的更新操做对于读请求有什么影响。下图为HBase基于行锁实现的数据更新时序示意图:
上图中简单地表述了数据更新流程(后续文章会对HBase数据写入进行深刻的介绍),简单来讲,数据更新能够分为以下几个阶段:获取行锁、更新WAL、数据写入本地缓存memstore、释放行锁。
如上图所示,先后分别有两次对同一行数据的更新操做。假如第二次更新过程在将列簇cf1更新为t2_cf1以后中有一次读请求进来,此时读到的第一列数据将是第二次更新后的数据t2_cf1,然而第二列数据倒是第一次更新后的数据t1_cf2,很显然,只针对更行操做加行锁会产生读取数据不一致的状况。最简单的数据不一致解决方案是读写线程公用一把行锁,这样能够保证读写之间互斥,可是读写线程同时抢占行锁必然会极大地影响性能。
为此,HBase采用MVCC解决方案避免读线程去获取行锁。MVCC解决方案对上述数据更新操做时序和读操做都进行了必定的修正,主要新增了一个写序号和读序号,其实就是数据的版本号。修正后的更新操做时序示意图为:
如上图所示,修正后的更新操做主要新增了‘获取写序号’和’结束写序号’两个步骤,而且每一个cell数据写memstore操做都会携带该写序号。那读请求须要通过什么样的修正呢?HBase的作法以下:
(1)每一个读操做开始时都会分配一个读序号,称为读取点
(2)读取点的值是全部的写操做完成序号中的最大整数
(3)一次读操做的结果就是读取点对应的全部cell值的集合
以下图所示,第一次更新获取的写序号为1,第二次更新获取的写序号为2。读请求进来时写操做完成序号中的最大整数为wn = 1,所以对应的读取点为wn = 1,读取的结果为wn = 1所对应的全部cell值集合,即为t1_cf1和t1_cf2,这样就能够实现以无锁的方式读取到一致的数据。
HBase中,MVCC的具体实现类为MultiVersionConsistencyControl,该类维护了两个long型的变量、一个WriteEntry对象和一个writeQueue队列:
1. long memstoreRead:记录当前全局的读取点,读请求进来以后首先会获取该读取点
2. long memstoreWrite:记录当前全局的写序号,根据它为下一个更新线程分配新的写序号
3. writeEntry:记录更新操做的写序号对象,主要包含两个变量,一个是writeNumber,表示写序号;一个是布尔类型的completed,表示该次更新是否完成
4. writeQueue:当前全部更新操做的写序号对象集合
根据上文中更新数据时序图可知,更新线程获取行锁以后就须要获取写序号,对应的方法为beginMemstoreInsert,该方法将memstoreWrite加1,生成writeEntry对象并插入到队列writeQueue,返回writeEntry对象。Note:生成的writeEntry对象中包含写序号writeNumber,更新线程会将该writeNumber设置为cell数据的一个属性。
数据更新完成以后,释放行锁以前,更新线程会调用completeMemstoreInsert方法更新writeEntry对象以及memstoreRead变量,具体分为以下两步:
1. 首先将该writeEntry对象标记为’已完成’,再将全局读取点memstoreRead尽量多地往前移。前移算法为遍历队列writeQueue中全部的writeEntry对象,移除掉已经标记为’已完成’的writeEntry直至遇到未完成的writeEntry,最后将memstoreRead变量更新为最新已完成的writeNumber。
2. 注意上述memstoreRead变量有可能并不等于当前更新线程的writeNumber,这种状况下该更新线程对数据的更新操做对用户并不可见。为了实现更新完成以后更新结果即对用户可见,须要等待memstoreRead变量前移到当前更新线程的witeNumber。所以它会阻塞当前线程,等待其余线程对应的writeEntry对象标记为’已完成’,直至memstoreRead等于当前线程的writeNumber。
HBase提供了各类锁机制和MVCC机制来保证数据的原子性、一致性等特性,其中使用互斥锁实现的行锁保证了行级数据的原子性,使用JDK提供的读写锁实现了Store级别、Region级别的数据一致性,同时使用行锁+MVCC机制实现了在高性能非锁定读场景下的数据一致性。