HBase MultiVersionConsistencyControl
注明:本文部分文字和图片翻译或引用自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的更多相关文章
- HBase中MVCC的实现机制及应用情况
MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,广泛使用于数据库系统.本文将介绍HBase中对于MVCC的实现及应用情况. MVCC基本原理 在介绍 ...
- HBase 事务和并发控制机制原理
作为一款优秀的非内存数据库,HBase和传统数据库一样提供了事务的概念,只是HBase的事务是行级事务,可以保证行级数据的原子性.一致性.隔离性以及持久性,即通常所说的ACID特性.为了实现事务特性, ...
- HBase MVCC 代码阅读(一)
MultiVersionConcurrencyControl.java,版本 0.94.1 MultiVersionConsistencyControl 管理 memstore 中的读写一致性.该类实 ...
- HBase MVCC 机制介绍
关键词:MVCC HBase 一致性 本文最好结合源码进行阅读 什么是MVCC ? MVCC(MultiVersionConsistencyControl , 多版本控制协议),是一种通过数据的多版本 ...
- HBase的写事务,MVCC及新的写线程模型
MVCC是实现高性能数据库的关键技术,主要为了读不影响写.几乎所有数据库系统都用这技术,比如Spanner,看这里.Percolator,看这里.当然还有mysql.本文说HBase的MVCC和0.9 ...
- hbase源码系列(十二)Get、Scan在服务端是如何处理?
继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以 ...
- hbase源码系列(十一)Put、Delete在服务端是如何处理?
在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTable探秘>的朋友都会有印象,没看过的建议回去先看看,Put是通过Mu ...
- hbase源码系列(七)Snapshot的过程
在看这一章之前,建议大家先去看一下snapshot的使用.可能有人会有疑问为什么要做Snapshot,hdfs不是自带了3个备份吗,这是个很大的误区,要知道hdfs的3个备份是用于防止网络传输中的失败 ...
- Hbase Region Server整体架构
Region Server的整体架构 本文主要介绍Region的整体架构,后续再慢慢介绍region的各部分具体实现和源码 RegionServer逻辑架构图 RegionServer职责 1. ...
随机推荐
- LINUX 内核与 systemtap +GO 专家博客 一个[ 系统软件工程师] 的随手涂鸦
http://nanxiao.me/category/%E3%80%8Anix-hacking%E3%80%8B%E6%9D%82%E5%BF%97/ 月刊 https://github.co ...
- retrofit2 okhttp3 RxJava butterknife 示例
eclipse的jar包配置 eclipse中貌似用不了butterknife buildToolsVersion "23.0.2" defaultConfig { applica ...
- jQuery创建ajax关键词数据搜索
在web开发过程当中,我们经常需要在前台页面输入关键词进行数据的搜索,我们通常使用的搜索方式是将搜索结果用另一个页面显示,这样的方式对于搭建高性能网站来说不是最合适的,今天给大家分享一下如何使用 jQ ...
- Electron
跨平台桌面app开发 Appjs hex nwjs electron 官网:http://electron.atom.io/ 中文文档:https://github.com/atom/electron ...
- 关于git的一些常用命令
1.git init 把目录变成Git可以管理的仓库 2.git add 把文件添加到仓库 3.git commit -m "" 把文件提交到仓库,-m后面是提交说明 4.git ...
- asp.net基础概念总结
1 什么是asp.net?asp.net是一种编程语言吗? asp.net是Microsoft公司推出的新一代建立动态web应用程序的开发平台,是一种建立动态web应用程序的新技术. 不是,asp. ...
- (转)ThinkPHP自定义模板标签详解
转之--http://www.thinkphp.cn/topic/6258.html 模板标签让网站前台开发更加快速和简单,这让本该由程序猿才能完成的工作,现在只要稍懂得HTM的人也能轻易做到,这也就 ...
- Android系统下的动态Dex加载与app速度优化
1 问题 在Android系统中,一个App的所有代码都在一个Dex文件里面.Dex是一个类似Jar的存储了多有Java编译字节码的归档文件.因为Android系统使用Dalvik虚拟机,所以需要把 ...
- 标量类型(scalar)
(ISO C11 §6.2.5) Arithmetic types and pointer types are collectively called scalar types. Array and ...
- 编译安装php时提示Cannot find MySQL header files的解决方法
php的配置文件中有一行--with-mysql=/usr/local/mysql ,安装的时候提示:configure: error: Cannot find MySQL header files ...