注明:本文部分文字和图片翻译或引用自http://blogs.apache.org/hbase/entry/apache_hbase_internals_locking_and。
HBase在保证高性能的同时,为用户提供了一致性的和便于理解的数据模型。
为了理解HBase的并发控制,我们首先需要明白HBase为什么需要并发控制,也就是说,HBase提供了哪些特性需要并发控制?
特性:HBase提供了基于行的ACID语义。
• Atomicity: All parts of transaction complete or none complete
• Consistency: Only valid data written to database
• Isolation: Parallel transactions do not impact each other’s execution
• Durability: Once transaction committed, it remains
原子性:一个事务的组成部分,要么全部成功,要么全部失败;
一致性:只有正确的数据才能被写入;
隔离性:并行的事务之间不能够相互影响;
持久性:事务一旦被提交,它将被永久地保存下来。
写写同步
考虑一个并发写入数据的情况:
假设有两个客户端实例,往HBase某表中同一行的同一个Family(Info)的两个Qualifier(Company、Role)写入数据,一般情况下,这两个写入请求会被HBase RegionServer接收后封装成两个Call,然后被两个Handler(线程)分别处理,即将请求中的数据写入列簇Company的MemStore中,对于MemStore来说,这些数据是被并发的线程写入的。
写入流程如下所示:
(1) Write to Write-Ahead-Log (WAL)
(2) Update MemStore: write each data cell [the (row, column) pair] to the memstore
注:这里忽略WAL的影响,新版本的HBase中相关部分已发生改变。
具体写入数据时是以KeyValue为数据单位的,对于上图中的数据来说,实际写入时有四个KeyValue,每个写入线程负责写入两个KeyValue,如果HBase没有相应的并发控制,则这四个KeyValue写入MemStore的顺序是无法预料的,可能会出现以下情况:
由上图可以看出,四个KeyValue的写入顺序为:
我们最后得到的结果为:
从ACID的语义出发,两次的写入并没有被隔离,导致最终的结果数据出现了数据交错的情况,因此,我们需要针对上述场景提供相应的并发控制策略。
最简单的一种方案是在对某一行进行操作之前,首先显式对该行进行加锁操作,加锁成功后才进行相应操作,否则只能等待获取锁,此时,写入流程如下:
(0) Obtain Row Lock
(1) Write to Write-Ahead-Log (WAL)
(2) Update MemStore: write each cell to the memstore
(3) Release Row Lock
引入行锁的机制后,就可以避免并发情景下,对同一行数据进行操作(写入或更新)时出现数据交错的情况。
读写同步
在HBase中写入数据时,我们加入了行锁机制用以保证ACID的语义,那么,在HBase中读取数据时,是否也需要并发控制策略呢,我们考虑下面的例子:
假设我们没有为HBase的读操作引入任何的并发控制策略,在两次写入请求的同时,再发起一个读取请求,这三个请求都是针对同一行进行读写操作的,如上图所示,如果读取请求正好在Waiter被写入MemStore之前被执行,则我们会得到下面的结果:
这个结果肯定不是我们所期望的,因此,我们需要引入相应的并发控制策略来进行读写同步。
最简单的方案是和写入一样,在读取操作前后分别加入获取锁与释放锁的步骤,这样虽然解决了ACID的问题,但是不管读取或写入都需要涉及到锁的操作,彼此之间产生竞争,极大地降低了系统的吞吐量。取而代之的是,HBase使用了一种“多版本一致性控制”的策略来避免读取的锁操作。
MultiVersionConsistencyControl(MVCC)
写入工作流程:
(w1) After acquiring the RowLock, each write operation is immediately assigned a write number
(w2) Each data cell in the write stores its write number.
(w3) A write operation completes by declaring it is finished with the write number.
读取工作流程:
(r1) Each read operation is first assigned a read number, called a read point.
(r2) The read point is assigned to be the highest integer x such that all writes with write number <= x have been completed.
(r3) A read r for a certain (row, column) combination returns the data cell with the matching (row, column) whose write number is the largest value that is less than or equal to the read point of r.
使用了MVCC策略的情形如下:
每一次写入操作之前都会被分配一个WriteNumber(w1),每一个数据单元(KeyValue)写入MemStore时都会携带着这个WriteNumber(w2),写入完成后提交这个WriteNumber(w3)。写入操作是锁机制下进行的。
现在考虑先前的读取操作,读取发生在Restaurant [wn=2]之后、Waiter [wn=2]之前,根据读取工作流程r1和r2可知,该次读取的ReadNumber(ReadPoint)被分配为1,再根据r3可知,它目前只能读取WriteNumber小于或等于1的数据,因此,读取结果如下:
由此可能看出,在MVCC的帮助下,即使没有锁,我们读取的结果也是一致性的。
总结一下,引入MVCC后写入操作流程如下:
(0) Obtain Row Lock
(0a) Acquire New Write Number
(1) Write to Write-Ahead-Log (WAL)
(2) Update MemStore: write each cell to the memstore
(2a) Finish Write Number
(3) Release Row Lock
MVCC源码分析
org.apache.hadoop.hbase.regionserver.MultiVersionConsistencyControl
MultiVersionConsistencyControl在创建HRegion时被初始化的。
MultiVersionConsistencyControl包含四个实例变量:
private volatile long memstoreRead = 0;
即上文中提到的ReadNumber。
private volatile long memstoreWrite = 0;
即上文中提到的WriteNumber。
private final Object readWaiters = new Object();
用作同步变量。
// This is the pending queue of writes. private final LinkedList<WriteEntry> writeQueue = new LinkedList<WriteEntry>();
用于保存所有尚未提交的WriteNumber。
WriteEntry的定义如下:
public static class WriteEntry { private long writeNumber; private boolean completed = false; WriteEntry(long writeNumber) { this.writeNumber = writeNumber; } void markCompleted() { this.completed = true; } boolean isCompleted() { return this.completed; } long getWriteNumber() { return this.writeNumber; } }
其中,writeNumber表示本次写入操作之前所分配的WriteNumber;completed用于表示本次WriteNumber是否被提交。
在每次写入操作之前,调用beginMemstoreInsert方法;
public WriteEntry beginMemstoreInsert() { synchronized (writeQueue) { long nextWriteNumber = ++memstoreWrite; WriteEntry e = new WriteEntry(nextWriteNumber); writeQueue.add(e); return e; } }
主要包含三个步骤:
(1)分配本次写入操作的WriteNumber;
(2)创建WriteEntry对象,并将其添加至写队列中;
(3)返回相应的WriteEntry对象。
在每次写入操作完成之后,需要根据写入操作之前的WriteEntry对象,完成WriteNumber的提交,这些工作是通过completeMemstoreInsert方法完成的:
public void completeMemstoreInsert(WriteEntry e) { advanceMemstore(e); waitForRead(e); }
主要包含两个步骤:
(1)根据本次提交的WriteNumber,调整ReadNumber;
(2)如果调整后的ReadNumber值小于e的WriteNumber,则本次完成操作需要被阻塞,数据还不能被读取,直到上述条件被破坏。
为什么提交WriteNumber时,会出现调整后的ReadNumber小于本次写操作所分配的WriteNumber呢?
这是因为并发写入时,多个线程的写入速度是随机的,可能存在WriteNumber比较大(假设值为x)的写入操作比WriteNumber较小的(假设值为y)写入操作先结束了,但此时并不能将ReadNumber的值调整为x,因为此时还存在WriteNumber比x小的写入操作正在进行中,ReadNumber为x即表示MemStore中所有WriteNumber小于或等于x的数据都可以被读取了,但实际上还有值没有被写入完成,可能会出现数据不一致的情况,所以如果写队列中WriteNumber比较大的写入操作如果较快的结束了,则需要进行相应的等待,直到写队列中它前面的那些写入操作完成为止。
advanceMemstore方法:
boolean advanceMemstore(WriteEntry e) { synchronized (writeQueue) { e.markCompleted(); long nextReadValue = -1; boolean ranOnce = false; while (!writeQueue.isEmpty()) { ranOnce = true; WriteEntry queueFirst = writeQueue.getFirst(); if (nextReadValue > 0) { if (nextReadValue + 1 != queueFirst.getWriteNumber()) { throw new RuntimeException( "invariant in completeMemstoreInsert violated, prev: " + nextReadValue + " next: " + queueFirst.getWriteNumber()); } } if (queueFirst.isCompleted()) { nextReadValue = queueFirst.getWriteNumber(); writeQueue.removeFirst(); } else { break; } } if (!ranOnce) { throw new RuntimeException("never was a first"); } if (nextReadValue > 0) { synchronized (readWaiters) { memstoreRead = nextReadValue; readWaiters.notifyAll(); } } if (memstoreRead >= e.getWriteNumber()) { return true; } return false; } }
标记本次写入操作完成。
e.markCompleted();
如果写队列不为空,循环处理写队列:
long nextReadValue = -1; boolean ranOnce = false; while (!writeQueue.isEmpty()) { ranOnce = true; WriteEntry queueFirst = writeQueue.getFirst(); if (nextReadValue > 0) { if (nextReadValue + 1 != queueFirst.getWriteNumber()) { throw new RuntimeException( "invariant in completeMemstoreInsert violated, prev: " + nextReadValue + " next: " + queueFirst.getWriteNumber()); } } if (queueFirst.isCompleted()) { nextReadValue = queueFirst.getWriteNumber(); writeQueue.removeFirst(); } else { break; } } if (!ranOnce) { throw new RuntimeException("never was a first");
}
(1)如果队首元素(WriteEntry)已被标记为完成,则更新nextReadValue,并移除该队首元素;
(2)如果队首元素未完成,则结束循环。
if (nextReadValue > 0) { synchronized (readWaiters) { memstoreRead = nextReadValue; readWaiters.notifyAll(); } }
如果nextReadValue大于0,表示有写入操作提交完成,则更新memstoreRead的值,并唤醒所有在readWaiters的等待线程。
if (memstoreRead >= e.getWriteNumber()) { return true; }
如果此时memstoreRead大于或等于本次写入操作的WriteNumber,则表示MemStore中所有WriteNumber的数据可以被读取了,返回true,否则会返回false。
waitForRead方法:
/** * Wait for the global readPoint to advance upto the specified transaction * number. */ public void waitForRead(WriteEntry e) { boolean interrupted = false; synchronized (readWaiters) { while (memstoreRead < e.getWriteNumber()) { try { readWaiters.wait(0); } catch (InterruptedException ie) { // We were interrupted... finish the loop -- i.e. cleanup // --and then // on our way out, reset the interrupt flag. interrupted = true; } } } if (interrupted) { Thread.currentThread().interrupt(); } }
该方法执行流程会被阻塞,直接当前的memstoreRead大于或等于本次写入操作的WriteNumber为止,此时MemStore中所有WriteNumber小于或等于memstoreRead或本次写入操作的WriteNumber的数据皆可被读取,本次写入操作亦也完成。