注明:本文部分文字和图片翻译或引用自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的数据皆可被读取,本次写入操作亦也完成。

HBase MultiVersionConsistencyControl的更多相关文章

  1. HBase中MVCC的实现机制及应用情况

    MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,广泛使用于数据库系统.本文将介绍HBase中对于MVCC的实现及应用情况. MVCC基本原理 在介绍 ...

  2. HBase 事务和并发控制机制原理

    作为一款优秀的非内存数据库,HBase和传统数据库一样提供了事务的概念,只是HBase的事务是行级事务,可以保证行级数据的原子性.一致性.隔离性以及持久性,即通常所说的ACID特性.为了实现事务特性, ...

  3. HBase MVCC 代码阅读(一)

    MultiVersionConcurrencyControl.java,版本 0.94.1 MultiVersionConsistencyControl 管理 memstore 中的读写一致性.该类实 ...

  4. HBase MVCC 机制介绍

    关键词:MVCC HBase 一致性 本文最好结合源码进行阅读 什么是MVCC ? MVCC(MultiVersionConsistencyControl , 多版本控制协议),是一种通过数据的多版本 ...

  5. HBase的写事务,MVCC及新的写线程模型

    MVCC是实现高性能数据库的关键技术,主要为了读不影响写.几乎所有数据库系统都用这技术,比如Spanner,看这里.Percolator,看这里.当然还有mysql.本文说HBase的MVCC和0.9 ...

  6. hbase源码系列(十二)Get、Scan在服务端是如何处理?

    继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以 ...

  7. hbase源码系列(十一)Put、Delete在服务端是如何处理?

    在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTable探秘>的朋友都会有印象,没看过的建议回去先看看,Put是通过Mu ...

  8. hbase源码系列(七)Snapshot的过程

    在看这一章之前,建议大家先去看一下snapshot的使用.可能有人会有疑问为什么要做Snapshot,hdfs不是自带了3个备份吗,这是个很大的误区,要知道hdfs的3个备份是用于防止网络传输中的失败 ...

  9. Hbase Region Server整体架构

    Region Server的整体架构 本文主要介绍Region的整体架构,后续再慢慢介绍region的各部分具体实现和源码 RegionServer逻辑架构图 RegionServer职责 1.    ...

随机推荐

  1. Android之开发常用颜色

    Android开发中常常要用一些个性化的颜色,然而茫茫的RBG颜色对照表,往往给人眼花缭乱的感觉,更别说从中轻易选出一两种比较满意的颜色,下面我就总结一下开发中常用到的比较绚丽的颜色,都是有名有姓的哦 ...

  2. PHP开发安全之近墨者浅谈(转)

    ==过滤输入/输出转义 过滤是Web应用安全的基础.它是你验证数据合法性的过程.通过在输入时确认对所有的数据进行过滤,你可以避免被污染(未过滤)数据在你的程序中被误信及误用.大多数流行的PHP应用的漏 ...

  3. 几种任务调度的 Java 实现方法与比较--转载

    前言 任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务.本文由浅入深介绍四种任务调度的 Java 实现: Timer ScheduledExecutor 开源工具包 Quartz ...

  4. iOS开发UI篇-懒加载、重写setter方法赋值

    一.懒加载 1.懒加载定义 懒加载——也称为延迟加载,即在需要的时候才加载(效率低,占用内存小).所谓懒加载,写的是其get方法. 注意:如果是懒加载的话则一定要注意先判断是否已经有了,如果没有那么再 ...

  5. Android(java)学习笔记229:服务(service)之绑定服务调用服务里面的方法 (采用接口隐藏代码内部实现)

    1.接口 接口可以隐藏代码内部的细节,只暴露程序员想暴露的方法 2.利用上面的思想优化之前的案例:服务(service)之绑定服务调用服务里面的方法,如下: (1)这里MainActivity.jav ...

  6. GridView禁止上下滚动的方法

    通常情况下,我们使用GridView来完成类似表格的布局,这种布局,我们只需要设置列数,会自动根据适配器的数据进行适配,非常灵活. GridView其实就是一个容器.允许向其内部添加控件,通常情况下, ...

  7. Poj 3368 Frequent values

    /* 线段树区间合并 维护几个信息 到时候乱搞一下就好了 开始T了 有一种情况可以不用递归 直接算出来 */ #include<iostream> #include<cstdio&g ...

  8. codevs4189字典(字典树)

    /* 本字典树较弱 只支持插入单词 查询单词. 特殊的 bool变量w 标记此字母是不是某个单词的结束 (然而这个题并没卵用) */ #include<iostream> #include ...

  9. jQuery绑定事件的四种基本方式

    Query中提供了四种事件监听方式,分别是bind.live.delegate.on,对应的解除监听的函数分别是unbind.die.undelegate.off. bind(type,[data], ...

  10. canvas --> getImageData()

    getImageData() 使用时有跨域问题 设置img的属性 crossOrigin="anonymous"可解决crossOrigin的问题 <img src=&quo ...