前言

讲解HBase事务的文章很多,这里就不过多赘述了,大家应该都知道是通过MVCC实现的。但是今天这篇文章的背景是一个同事和我讨论一个问题引发的,这个问题使我重新梳理下这块内容并作为记录和大家分享。

下面先来看看这个问题:

HBase的查询流程是:先查询MemStore,查不到则查询BlockCache,还没有则查询HFile,再将查询到的数据放入BlockCache。

请问是不是存在这么一种情况,假如有一条数据id=1,name='张三',当被查询时,数据被放入blockCache中,后来数据更新了,id=1,name='李四',数据存入memstore中,达到刷写机制,写入hfile中了。用户查询这条数据,发现blockCache中有这条数据,但是数据是旧的,name='张三'。于是取到了旧的数据

其实这是个很常见的场景,我当时第一反应是肯定不会查到脏数据,但是到底怎么实现的,我还真的一时有点拿不准了,作为自己最熟悉的组件,这个问题还是需要弄清楚的。

其实上面的问题仔细分析下,可以分解为下面几个问题:

  • BlockCache中存的是什么?

  • 肯定有数据标识标记数据的版本使得取数据不会出现问题,那这个标识是什么?

  • HBase是如何保持数据一致性的?

下面就从这几个问题展开并解答上面的那个问题。

正文

BlockCache中存的是什么

这个问题不是今天这个问题的核心内容,但是当做准备知识顺便说一下还是可以的。

BlockCache说明

  • BlockCache称为读缓存,主要是加速HBase读取数据的速度的。与之对应的是HBase的写缓存,即MemStore,用以加速HBase的写操作。

  • HBase会将一次文件查找的Block块缓存到Cache中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的IO操作。

BlockCache缓存的数据

看完BlockCache的作用后,下面来看看BlockCache里面到底存了什么?

其实答案很简单,都叫BlockCache了,里面肯定存的是Block了。这里要区分两个概念,由于HBase底层使用HDFS存储,而HDFS也有Block的概念,所以要区分这两个Block概念。

  • HDFS的Block是HDFS维度的概念,这个Block的大小默认是128M;

  • HBase的Block是HBase维度的概念,这个Block的大小默认是64K;

从大小比较就能看出来两者的区别,如果使用HDFS的Block粒度来做缓存,那么缓存不了几个Block,BlockCache就满了,显然不合适。而HBase的Block默认的64K大小是一个基于HBase的两种查询操作scan和get效率的一个折中。如果get操作居多,可以适当调小Block的大小;反之如果scan操作居多,则可以适当调大Block的大小。如果两者数量差不多,那妥妥的默认即可。

那么HBase都有哪些Block呢?主要分为下面四种:

  • Data Block:用于存储实际数据,通常情况下每个Data Block可以存放多条KeyValue数据对,这些数据都是查询之后包含结果的Block在BlockCache中的缓存,用以加速查询。

  • Index Block:用于存储硬盘上数据的索引文件,通过存储索引数据加快数据查找

  • Bloom Block:用于存储Hfile中rowkey的布隆过滤器,用于过滤掉部分一定不存在KeyValue的数据查询,减少不必要的IO操作。

  • Meta Block:存储整个HFile的元数据,包含HFile的基本信息,布隆过滤器的元数据,HFile的数据以及索引的原信息等。

BlockCache分类

这个简单说说,当前HBase主流的BlockCache使用方式就是将BucketCache和LruBlockCache搭配使用,称为CombinedBlockCache即CBC。至于BucketCache和LruBlockCache具体说明大家不清楚的可以去查一下,网上很多,这里我简单说说这两者的特点:

  • LruBlockCache中主要存储Index Block和Bloom Block,采用LRU算法进行缓存淘汰。而Meta Block以及被设置为IN_MEMORY => 'true'的内存表不参与LRU淘汰过程而常驻内存。

  • BucketCache中主要存储Data Block

  • 一次随机读需要首先在LruBlockCache中查到对应的Index Block,然后再到BucketCache查找对应数据块

  • Bucket Cache缓存中有3种模式:heap模式和offheap模式file模式,常规的heap模式不过多介绍,offheap模式因为内存属于操作系统,所以基本不会产生各种GC,尤其是产生毛刺的Full GC。而file模式借助于SSD以及Alluxio等存储也可以实现高速查询。

HBase是如何避免脏读的

首先先纠正下上面问题的读取数据流程。上面数据读取数据流程其实是错误的,那么正确的流程是什么样子的呢?其实HBase的查询操作分为Get和Scan操作两种(虽然Get操作也是被当做Scan来处理)流程如下:

  • 首先确实需要查询Memstore,但是并不是查询到数据就返回,而是在查询Memstore的同时也会去文件中查询,最后会将结果进行合并筛选。

  • 至于查hfile,则是先根据rowkey段进行筛选,选出符合条件的HFile(如果是get操作,布隆过滤器应该会起作用,能直接筛选出更精确的文件)。然后判断hfile对应的block是否在blockcache中,如果存在就直接读取blockcache的数据,不存在就加载对应block到blockcache中并查询对应的数据。

  • 所以针对上面的场景,如果数据所在的region没有发生compact的话,应该会返回两个结果,一个在BlockCache中;另一个在文件中,被加载到BlockCache中后被查询出来。

  • 然后关键的地方来了,其实在查询的时候scan是带有读序号的,而数据存储中也是带有写序号的,最后会按rowkey将收集来的所有结果分组,然后根据读序号和写序号的关系来选取唯一符合条件的值。筛选条件就是Max(写序号<=读序号的所有值)

上面就是HBase避免脏读的处理手段。乍看起来信息量有点大,大家可能有点懵,什么是读序号,什么是写序号,这两者之间是什么关系以及如何同步的?这就是涉及到了HBase事务实现机制MVCC的一些细节,下文详细详解。

HBase是如何保持数据一致性的

上文讲了HBase是如何避免脏读的,下面就来看看上面的那一些专有名词以及HBase是如何保持查询一致以及数据一致的。

首先MVCC相关的基础知识我这里就不赘述了,大家可以去网上查查,资料很多,我这里主要讲讲MVCC这个组件在HBase中是如何工作,以及读序号和写序号是如何关联以及更新的,进而就可以回答上述的那个问题。先看下图:

首先MVCC有三个主要组成部分:

  • writePoint:写序号,AtomicLong类型

  • readPoint:读序号,AtomicLong类型

  • LinkedList<WriteEntry> writeQueue:存储写操作状态的list,之所以选用LinkedList,是因为这个list需要频繁在两头插入和删除WriteEntry。

MVCC每个region都有一个实例。这三个属性通过规则联动,HBase读写该region的数据都会从这里获取读写序号,然后进行相关的操作。

下面来看看这三个属性的联动规则:

1.当一个client写入数据时,首先lock住MVCC控制中心的写入队列writeQueue,并向其插入一个新的entry,并将之前的writePoint+1赋予entry的writeNumber(writePoint+1也是同步操作),表示发起了一个新的写入事务。completed值此时为False,表名目前事务还未完成,数据还在写入过程中。图中的write client1和write client3就处于这个阶段。

2.第二步client将数据写入memstore和WAL,此时认为数据已经持久化,可以结束该事务。此处需要注意,这里只是事务结束,但是并没有返回客户端写入成功,还需要有下面MVCC相关的操作。

3.client调用MVCC控制中心的complete(WriteEntry writeEntry)方法,该方法对writeQueue采用synchronized关键字,将该num对应的entry的completed设置为True,表示该entry对应的事务完成。但是单单将completed设置为True是不够的,我们的最终目的是要让scan能够看到最新写入完成的数据,也就是说还需要更新readPoint。

/*** Mark the {@link WriteEntry} as complete and advance the read point as much as possible.* Call this even if the write has FAILED (AFTER backing out the write transaction* changes completely) so we can clean up the outstanding transaction.** How much is the read point advanced?** Let S be the set of all write numbers that are completed. Set the read point to the highest* numbered write of S.** @param writeEntry** @return true if e is visible to MVCC readers (that is, readpoint >= e.writeNumber)*/public boolean complete(WriteEntry writeEntry) {synchronized (writeQueue) {writeEntry.markCompleted();long nextReadValue = NONE;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 complete violated, nextReadValue="+ nextReadValue + ", writeNumber=" + queueFirst.getWriteNumber());}}if (queueFirst.isCompleted()) {nextReadValue = queueFirst.getWriteNumber();writeQueue.removeFirst();} else {break;}}if (!ranOnce) {throw new RuntimeException("There is no first!");}if (nextReadValue > 0) {synchronized (readWaiters) {readPoint.set(nextReadValue);readWaiters.notifyAll();}}return readPoint.get() >= writeEntry.getWriteNumber();}}

4.更新readPoint:同样在complete(WriteEntry writeEntry)方法中完成,每一个client将其对应的entry的completed设置为True后,都会去按照队列顺序,从readPoint开始遍历,假如遍历到的entry的completed为True,则将readPoint更新至此位置,直到遇到completed为False的位置时停止。也就是说每个client写入之后,都会尽力去将readPoint更新到目前最大连续的已经完成的事务的点(因为是有可能后开始的事务先于之前的事务完成)。

看到这里,可能大家会想了,那假如事务A先于事务C,事务A还未完成,但事务C已经完成,事务C也只能将readPoint更新到事务A之前的位置,如果此时事务C返回写入成功,那按道理来说scan是应该能够查到事务C的数据,但是由于readPoint没有更新到C,就会造成一个现象就是:事务C明明提示执行成功,但是查询的时候却看不到。

所以上面说的第4步其实还并没有完,client在执行complete(WriteEntry writeEntry)后,如果方法返回的值为false,还会执行一个waitForRead(WriteEntry e)方法,参数的entry就是该事务对应的entry,下面是源码逻辑:

/*** Complete a {@link WriteEntry} that was created by {@link #begin()} then wait until the* read point catches up to our write.** At the end of this call, the global read point is at least as large as the write point* of the passed in WriteEntry.  Thus, the write is visible to MVCC readers.*/public void completeAndWait(WriteEntry e) {if (!complete(e)) {waitForRead(e);}}

该方法会一直等待readPoint大于等于该entry的writeNumber时才会返回,这样保证了事务有序完成。此时客户端才会最终返回写入成功,即下次查询就会查询到最新的数据。下面是源码逻辑:

/*** Wait for the global readPoint to advance up to the passed in write entry number.*/void waitForRead(WriteEntry e) {boolean interrupted = false;int count = 0;synchronized (readWaiters) {while (readPoint.get() < e.getWriteNumber()) {if (count % 100 == 0 && count > 0) {long totalWaitTillNow = READPOINT_ADVANCE_WAIT_TIME * count;LOG.warn("STUCK for : " + totalWaitTillNow + " millis. " + this);}count++;try {readWaiters.wait(READPOINT_ADVANCE_WAIT_TIME);} 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();}}

再回到上面那个图,当前write client 2在等待write client 3写入成功后readPoint追上来,所以write client 2处于写入成功等待readPoint追上来的阶段。此时的readPoint是6,查询的时候只能查询到writePoint <= 6的数据,然后返回其中writePoint最大的数据。而writePoint = 8的数据虽然写入成功,但是客户端并没有收到写入成功的状态,数据不可见也符合一般认知。

以上就是HBase写入时MVCC的工作流程,scan就比较好理解了,每一个scan请求都会申请一个readPoint,保证了该readPoint之后的事务不会被检索到。

另外,上述的HBase查询机制是基于HBase默认的事务级别即read committed级别。同时HBase也同样支持read uncommitted级别,也就是我们在查询的时候将scan的mvcc值设置为一个超大的值,大于目前所有申请的MVCC值,那么查询时同样会返回正在写入的数据。

/*** Specify Isolation levels in Scan operations.* <p>* There are two isolation levels. A READ_COMMITTED isolation level* indicates that only data that is committed be returned in a scan.* An isolation level of READ_UNCOMMITTED indicates that a scan* should return data that is being modified by transactions that might* not have been committed yet.*/
@InterfaceAudience.Public
public enum IsolationLevel {READ_COMMITTED(1),READ_UNCOMMITTED(2);IsolationLevel(int value) {}public byte [] toBytes() {return new byte [] { toByte() };}public byte toByte() {return (byte)this.ordinal();}public static IsolationLevel fromBytes(byte [] bytes) {return IsolationLevel.fromByte(bytes[0]);}public static IsolationLevel fromByte(byte vbyte) {return IsolationLevel.values()[vbyte];}
}

总结

最后回到最上面的问题,查询的client会查询到张三和李四两条数据,但是由于李四是小于等于readPoint所有数据中writePoint最大的数据,所以最终返回客户端的数据是李四,结果符合预期,没有问题,上面的问题就是这样的。

回顾全文可以看出,HBase一条查询API后面执行的业务逻辑还是相当复杂的。如果作为初级人员,调用API即可,剩下的事情HBase就帮你做了。但是如果要进阶到HBase的高级阶段的话,这些原理性的东西还是需要了解和掌握的,只有掌握了这些原理,才会在HBase的问题定位以及性能优化上有好的发挥。

最后,如果想一起大数据的小伙伴,欢迎点赞转发加关注,下次学习不迷路,我们在大数据的路上共同前进!

挂个公众号二维码,公众号的文章是最新的,CSDN的会有些滞后,想追更的朋友欢迎大家关注公众号,谢谢大家支持。

公众号地址:

一个HBase查询问题引发的思考,作为HBase使用者这个问题你知道答案吗?相关推荐

  1. 罗生门:一个简单查询实现引发的思考

    站在不同的角度看待同一个问题,会得出不同的结论.对于程序的实现也一样. 一.查询功能实现的罗生门 不久前公司遇到一个场景,也是微服务状态下肯定会遇到的一个场景(以下对比真实情况有所抽象和改编): 某一 ...

  2. hbase查询_【从零单排HBase】HBase高性能查询揭秘

    先给结论吧:HBase利用compaction机制,通过大量的读延迟毛刺和一定的写阻塞,来换取整体上的读取延迟的平稳. 1.为什么要compaction 在上一篇 HBase读写 中我们提到了,HBa ...

  3. 一个知乎提问引发的(思考)[https://www.zhihu.com/question/263431508/answer/574084280]

    表示很喜欢这个问题,深有同感!这个问题也让我这种杂家谈谈想法吧,看题主应该是骨骼精奇的奇才,我假想读者是"小学生",所以,觉得我啰嗦的大大,忍忍吧,欢迎讨论. 先说回答,再说废话. ...

  4. 一个日常 Excel 公式引发的思考

    偶尔有朋友咨询我EXCEL公式问题,其实平日用得不多,就熟悉几个简单的函数.只是多数问题通过分解,是可以通过简单的函数实现的. 实际问题 以有效投标人平均价为基准价,每超出基准价1%扣0.5分,低于基 ...

  5. hbase 查询_云HBase发布全文索引服务,轻松应对复杂查询

    云HBase发布了"全文索引服务"功能,自2019年01月25日后创建的云HBase实例,可以在控制台免费开启此"全文索引服务"功能.使用此功能可以让用户在HB ...

  6. sql 账号查询一个表查询权限_一个查询语句引发的问题以及巨型表相关操作探索与思考...

    背景: 关于这个标题想了试了好几个总觉得欠那么点意思.大致情况是,在某服务支持中,1张大表4.5T左右,该表也是分区表.其中一个执行频繁的SQL写法有很大问题,导致巨表全量扫描,造成IO负载很大,业务 ...

  7. 一个分组查询引发的思考

    一个分组查询引发的思考 我们在看项目代码或者SQL语句时, 往往会看到很多非常复杂的业务或者SQL 那么问题来了. 复杂SQL是如何写成的? 下面通过一个数据展示的需求来体会到复杂的SQL是如何书写的 ...

  8. 一个小程序引发的思考

    既然是一个小程序引发的思考,那么我们就先看看这个小程序,看看他有何神奇之处: namespace ConsoleApplication1 {class Program{static void Main ...

  9. Spring之LoadTimeWeaver——一个需求引发的思考---转

    原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver--一个需求引 ...

最新文章

  1. 操作系统学习笔记 第六章:设备管理(王道考研)
  2. jQuery发送含有数组参数的ajax请求以及后台Struts2的OGNL解析错误
  3. Scanpy(三)可视化函数
  4. 理解smart pointer之三:unique_ptr
  5. C++对象模型5——类对象的内存布局
  6. STL源码剖析 第七章 仿函数(函数对象)
  7. UVA 11825 Hackers' Crackdown 状态DP
  8. java string hash变量_java基础(六)-----String性质深入解析
  9. 30年前的中专相当于现在什么学历?比现在一本厉害吗?
  10. ios android 通用字体,教你如何在iOS项目中设置各种字体
  11. 使用idea导出数据库脚本
  12. 《通信原理》awgn信道仿真
  13. Kesci:Tensorflow 实现 LSTM——时间序列预测(超详细)
  14. vue--后台管理系统问题和功能实现思路集锦
  15. windows xp 系统CMD命令大全(一)
  16. 雅虎口碑将关闭站长天下服务平台
  17. LSTM 长短期记忆神经网络及股票预测实现
  18. 陀螺仪重力感应(the gyroscope gravity induction)and (core Motion Framework)
  19. java1.7 apk 签名_【keytool jarsigner工具的使用】Android 使用JDK1.7的工具 进行APK文件的签名,以及keystore文件的使用...
  20. Django组件拾忆

热门文章

  1. PyTorch深度学习实践——对维度的认识
  2. 嘻哈艺术家和设计师Karan使用ThisIsKay.xyz来凸显他的音乐
  3. 【linux】端口讲解
  4. gpio信号过冲问题
  5. 到底该怎么喝牛奶才对?
  6. 2022年仿制药行业研究报告
  7. 达沃斯的AI思想交锋后,下一个实用人工智能风向标在哪?
  8. Ubuntu18.04安装Teamviewer-Host最简单的方法
  9. python 引用其他目录py文件_Python引用其他文件夹下的py文件
  10. 【JavaScript】实现仿windows计算器(完整版)