在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在hbase当中单独拿了一个工程出来实现了Trie的数据结果,既达到了压缩编码的效果,亦达到了方便查询的效果,一举两得,设置的方法是在上一章的末尾提了。

  下面讲一下这个Trie树的原理吧。

  树里面有3中类型的数据结构,branch(分支)、leaf(叶子)、nub(节点)

1、branch 分支节点,比如图中的t,以它为结果的词并没有出现过,但它是to、tea等次的分支的地方,单个t的词没有出现过。

2、leaf叶子节点,比如图中的to,它下面没有子节点了,并且出现了7次。

3、nub节点,它是结余两者之间的,比如i,它独立出现了11次。

  下面我们就具体说一下在hbase的工程里面它是什么样子的,下面是一个例子:

* Example inputs (numInputs=):
* : AAA
* : AAA
* : AAB
* : AAB
* : AAB
* : AABQQ
* : AABQQ
* <br/><br/>
* Resulting TokenizerNodes:
* AA <- branch, numOccurrences=, tokenStartOffset=, token.length=
* A  <- leaf, numOccurrences=, tokenStartOffset=, token.length=
* B  <- nub, numOccurrences=, tokenStartOffset=, token.length=
* QQ <- leaf, numOccurrences=, tokenStartOffset=, token.length= 

  这里面3个辅助字段,numOccurrences(出现次数)、tokenStartOffset(在原词当中的位置)、token.length(词的长度)。

  描述这个数据结构用了两个类Tokenizer和TokenizerNode。

  好,我们先看一下发起点PrefixTreeCodec,这个类是继承自DataBlockEncoder接口的,DataBlockEncoder是专门负责编码压缩的,它里面的有3个重要的方法,encodeKeyValues(编码)、decodeKeyValues(反编码)、createSeeker(创建扫描器)。

因此我们先看PrefixTreeCodec里面的encodeKeyValues方法,这个是我们的入口,我们发现internalEncodeKeyValues是实际编码的地方。

private void internalEncodeKeyValues(DataOutputStream encodedOutputStream,
      ByteBuffer rawKeyValues, boolean includesMvccVersion) throws IOException {
    rawKeyValues.rewind();
    PrefixTreeEncoder builder = EncoderFactory.checkOut(encodedOutputStream, includesMvccVersion);

    try{
      KeyValue kv;
      while ((kv = KeyValueUtil.nextShallowCopy(rawKeyValues, includesMvccVersion)) != null) {
        builder.write(kv);
      }
      builder.flush();
    }finally{
      EncoderFactory.checkIn(builder);
    }
}

   可以看到从rawKeyValues里面不断读取kv出来,用PrefixTreeEncoder.write方法来进行编码,最后调用flush进行输出。

  我们现在就进入PrefixTreeEncoder.write的方法里面吧。

rowTokenizer.addSorted(CellUtil.fillRowRange(cell, rowRange));
addFamilyPart(cell);
addQualifierPart(cell);
addAfterRowFamilyQualifier(cell);

  这里就跳到Tokenizer.addSorted方法里面

  public void addSorted(final ByteRange bytes) {
    ++numArraysAdded;
    //先检查最大长度,如果它是最大,改变最大长度
    if (bytes.getLength() > maxElementLength) {
      maxElementLength = bytes.getLength();
    }
    if (root == null) {
      // 根节点
      root = addNode(null, 1, 0, bytes, 0);
    } else {
      root.addSorted(bytes);
    }
  }

  如果root节点为空,就new一个root节点出来,有了根节点之后,就把节点添加到root节点的孩子队列里面。

  下面贴一下addSorted的代码吧

public void addSorted(final ByteRange bytes) {// recursively build the tree

    /*
     * 前缀完全匹配,子节点也不为空,取出最后一个节点,和最后一个节点也部分匹配
     * 就添加到最后一个节点的子节点当中
     */
    if (matchesToken(bytes) && CollectionUtils.notEmpty(children)) {
      TokenizerNode lastChild = CollectionUtils.getLast(children);
      //和最后一个节点前缀部分匹配
      if (lastChild.partiallyMatchesToken(bytes)) {
        lastChild.addSorted(bytes);
        return;
      }
    }
//匹配长度
    int numIdenticalTokenBytes = numIdenticalBytes(bytes);// should be <= token.length
    //当前token的起始长度是不变的了,剩余的尾巴的其实位置
    int tailOffset = tokenStartOffset + numIdenticalTokenBytes;
    //尾巴的长度
    int tailLength = bytes.getLength() - tailOffset;

    if (numIdenticalTokenBytes == token.getLength()) {
      //和该节点完全匹配
      if (tailLength == 0) {// identical to this node (case 1)
        incrementNumOccurrences(1);
      } else {
        // 加到节点的下面,作为孩子
        int childNodeDepth = nodeDepth + 1;
        int childTokenStartOffset = tokenStartOffset + numIdenticalTokenBytes;
        TokenizerNode newChildNode = builder.addNode(this, childNodeDepth, childTokenStartOffset, bytes, tailOffset);
        addChild(newChildNode);
      }
    } else {      split(numIdenticalTokenBytes, bytes);
    }
  }

1、我们先添加一个AAA进去,它是根节点,parent是null,深度为1,在原词中起始位置为0。

2、添加一个AAA,它首先和之前的AAA相比,完全一致,走的是incrementNumOccurrences(1),出现次数(numOccurrences)变成2。

3、添加AAB,它和AAA相比,匹配的长度为2,尾巴长度为1,那么它走的是这条路split(numIdenticalTokenBytes, bytes)这条路径

protected void split(int numTokenBytesToRetain, final ByteRange bytes) {
    int childNodeDepth = nodeDepth;
    int childTokenStartOffset = tokenStartOffset + numTokenBytesToRetain;

    //create leaf AA 先创建左边的节点
    TokenizerNode firstChild = builder.addNode(this, childNodeDepth, childTokenStartOffset,
      token, numTokenBytesToRetain);
    firstChild.setNumOccurrences(numOccurrences);// do before clearing this node's numOccurrences
    //这一步很重要,更改原节点的长度,node节点记录的数据不是一个简单的byte[]
    token.setLength(numTokenBytesToRetain);//shorten current token from BAA to B
    numOccurrences = 0;//current node is now a branch

    moveChildrenToDifferentParent(firstChild);//point the new leaf (AA) to the new branch (B)
    addChild(firstChild);//add the new leaf (AA) to the branch's (B's) children

    //create leaf 再创建右边的节点
    TokenizerNode secondChild = builder.addNode(this, childNodeDepth, childTokenStartOffset,
      bytes, tokenStartOffset + numTokenBytesToRetain);
    addChild(secondChild);//add the new leaf (00) to the branch's (B's) children

    // 递归增加左右子树的深度
    firstChild.incrementNodeDepthRecursively();
    secondChild.incrementNodeDepthRecursively();
  }

split完成的效果:

1) 子节点的tokenStartOffset 等于父节点的tokenStartOffset 加上匹配的长度,这里是0+2=2

2)创建左孩子,token为A,深度为父节点一致,出现次数和父亲一样2次

3)父节点的token长度变为匹配长度2,即(AA),出现次数置为0

4)把原来节点的子节点指向左孩子

5)把左孩子的父节点指向当前节点

6)创建右孩子,token为B,深度为父节点一致

7)把右孩子的父节点指向当前节点

8)把左右孩子的深度递归增加。

4、 添加AAB,和AA完全匹配,最后一个孩子节点AAB也匹配,调用AAB节点的addSorted(bytes),因为是完全匹配,所以和第二步一样,B的出现次数加1

5、添加AABQQ,和AA完全匹配,最后一个孩子节点AAB也匹配,调用AAB节点的addSorted(bytes), 成为AAB的孩子

先走的这段代码,走进递归

if (matchesToken(bytes) && CollectionUtils.notEmpty(children)) {
      TokenizerNode lastChild = CollectionUtils.getLast(children);
      //和最后一个节点前缀部分匹配
      if (lastChild.partiallyMatchesToken(bytes)) {
        lastChild.addSorted(bytes);
        return;
      }
}

然后再走的这段代码

int childNodeDepth = nodeDepth + 1;
int childTokenStartOffset = tokenStartOffset + numIdenticalTokenBytes;
TokenizerNode newChildNode = builder.addNode(this, childNodeDepth, childTokenStartOffset,
          bytes, tailOffset);
addChild(newChildNode); 

6、添加AABQQ,和之前的一样,这里就不重复了,增加QQ的出现次数

  构建玩Trie树之后,在flush的时候还做了很多操作,为这棵树构建索引信息,方便查询,这块博主真的无能为力了,不知道怎么才能把这块讲好。

hbase源码系列(五)Trie单词查找树的更多相关文章

  1. 10 hbase源码系列(十)HLog与日志恢复

    hbase源码系列(十)HLog与日志恢复   HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢 ...

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

    hbase源码系列(十一)Put.Delete在服务端是如何处理?    在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...

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

    hbase源码系列(十二)Get.Scan在服务端是如何处理?   继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...

  4. 9 hbase源码系列(九)StoreFile存储格式

    hbase源码系列(九)StoreFile存储格式    从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去 ...

  5. HBase源码系列之HFile

    本文讨论0.98版本的hbase里v2版本.其实对于HFile能有一个大体的较深入理解是在我去查看"到底是不是一条记录不能垮block"的时候突然意识到的. 首先说一个对HFile ...

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

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

  7. hbase源码系列(十五)终结篇&Scan续集-->如何查询出来下一个KeyValue

    这是这个系列的最后一篇了,实在没精力写了,本来还想写一下hbck的,这个东西很常用,当hbase的Meta表出现错误的时候,它能够帮助我们进行修复,无奈看到3000多行的代码时,退却了,原谅我这点自私 ...

  8. hbase源码系列(一)Balancer 负载均衡

    看源码很久了,终于开始动手写博客了,为什么是先写负载均衡呢,因为一个室友入职新公司了,然后他们遇到这方面的问题,某些机器的硬盘使用明显比别的机器要多,每次用hadoop做完负载均衡,很快又变回来了. ...

  9. 框架源码系列五:学习源码的方法(学习源码的目的、 学习源码的方法、Eclipse里面查看源码的常用快捷键和方法)

    一. 学习源码的目的 1. 为了扩展和调优:掌握框架的工作流程和原理 2. 为了提升自己的编程技能:学习他人的设计思想.编程技巧 二. 学习源码的方法 方法一: 1)掌握研究的对象和研究对象的核心概念 ...

  10. hbase源码系列(十四)Compact和Split

    先上一张图讲一下Compaction和Split的关系,这样会比较直观一些. Compaction把多个MemStore flush出来的StoreFile合并成一个文件,而Split则是把过大的文件 ...

随机推荐

  1. 在ubuntu16安装supervisor 并 启动 .net core.

    1. 安装supervisor apt-get install supervisor 2.新建supervisor配置文件.使用以下命令在linux 命令行 echo_supervisord_conf ...

  2. activiti排他网关

    /*启动流程实例*可以在启动流程时把所有流程变量设置好*/@Test public void startProcessInstance(){ //流程定义key String processDefin ...

  3. IOS使用AVAudioPlayer播放mp3歌曲文件并监听来电打断

    本实例实现了AVAudioPlayer播放mp3歌曲文件,实现了播放.暂停.继续操作,音乐音量控制.播放进度显示,同时监听来电打断事件 一.控件初始化 - (void)viewDidLoad { [s ...

  4. 浅谈HTML5 WebSocket的机制

    回想上一章 在上一章<为什么我们须要HTML5 WebSocket>中,我简单的介绍了下WebSocket的前世今生.相信大家已对WebSocket有了初步的了解.那么今天我们继续深入学习 ...

  5. profile bashrc bash_profile 之间的区别和联系

    /etc/profile:此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置. /etc/bashrc:为每一个 ...

  6. 找个些有用的网站(CSS生成)

    http://www.shejidaren.com/css-button-generator-and-beautifucl-style-sheet.html

  7. java命令执行jar包的方式

    http://www.cnblogs.com/adolfmc/archive/2012/10/07/2713562.html 大家都知道一个java应用项目可以打包成一个jar,当然你必须指定一个拥有 ...

  8. jetty 7.0 笔记

    codehaus  download:http://dist.codehaus.org/jetty/ 找到7.0  tar.gz 解压 拉取 Lib下jar 和  Lib--jsp下jar 代码同启动 ...

  9. RTX——第19章 SVC 中断方式调用用户函数(后期补历程)

    以下内容转载自安富莱电子: http://forum.armfly.com/forum.php 本章节为大家讲解如何采用 SVC 中断方式调用用户函数. 当用户将 RTX 任务设置为工作在非特权级模式 ...

  10. 设计模式之单一职责原则(iOS开发,代码用Objective-C展示)

    单一职责原则:就一个类而言,应该只有一个引起它变化的原因. 在iOS开发中,我们会很自然的给一个类添加各种各样的功能,比如随便写一个简单的应用程序,一般都会生成一个viewController类,于是 ...