hbase源码系列(五)Trie单词查找树

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

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

214e47591317c2e48f2042ad89fafb6ff1f53e6d

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

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

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

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

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

* Example inputs (numInputs=7): 
* 0: AAA 
* 1: AAA 
* 2: AAB 
* 3: AAB 
* 4: AAB 
* 5: AABQQ 
* 6: AABQQ 
* <br/><br/> 
* Resulting TokenizerNodes: 
* AA <- branch, numOccurrences=0, tokenStartOffset=0, token.length=2 
* A  <- leaf, numOccurrences=2, tokenStartOffset=2, token.length=1 
* B  <- nub, numOccurrences=3, tokenStartOffset=2, token.length=1 
* QQ <- leaf, numOccurrences=2, tokenStartOffset=3, token.length=2
这里面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。

f848aff0d1ddaad46aa2b83e974e0a401605c8b6

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完成的效果:

d2fe842fd4604aab2c92249c18dc2352580daaea

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

7d42e5589dcda95f45e52d4a38bec52503e6c77d

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); 

4acd330f539b28fc1aa71b145498bc23e53c9e6a

6、添加AABQQ,和之前的一样,这里就不重复了,增加QQ的出现次数。
aeff290da80fbd0d65968925e74e4398da608372
构建玩Trie树之后,在flush的时候还做了很多操作,为这棵树构建索引信息,方便查询,这块博主真的无能为力了,不知道怎么才能把这块讲好。
相关实践学习
云数据库HBase版使用教程
&nbsp; 相关的阿里云产品:云数据库 HBase 版 面向大数据领域的一站式NoSQL服务,100%兼容开源HBase并深度扩展,支持海量数据下的实时存储、高并发吞吐、轻SQL分析、全文检索、时序时空查询等能力,是风控、推荐、广告、物联网、车联网、Feeds流、数据大屏等场景首选数据库,是为淘宝、支付宝、菜鸟等众多阿里核心业务提供关键支撑的数据库。 了解产品详情:&nbsp;https://cn.aliyun.com/product/hbase &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
10月前
|
分布式数据库 Hbase
|
Java 分布式数据库 Ruby
HBase Filter 过滤器之 Comparator 原理及源码学习
HBase所有的比较器实现类都继承于父类ByteArrayComparable,而ByteArrayComparable又实现了Comparable接口;不同功能的比较器差别在于对父类compareTo()方法的重写逻辑不同。 下面分别对HBase Filter默认实现的七大比较器一一进行介绍。 1. BinaryComparator 介绍:二进制比较器,用于按字典顺序比较指定字节数组。 先看一个小例子: public class BinaryComparatorDemo { public static void main(String[] args) {
386 0
|
分布式数据库 Hbase
HBase 源码解析
HBase Read读流程源码解析&HBase Write写流程源码解析 &HBase Flush & Compact流程源码解析
4330 0
|
分布式数据库 Hbase 分布式计算
hbase源码系列(十五)终结篇&Scan续集-->如何查询出来下一个KeyValue
这是这个系列的最后一篇了,实在没精力写了,本来还想写一下hbck的,这个东西很常用,当hbase的Meta表出现错误的时候,它能够帮助我们进行修复,无奈看到3000多行的代码时,退却了,原谅我这点自私的想法吧。
3350 0
|
Java
hbase源码系列(十四)Compact和Split
本文介绍hbase中的Compact和Split。
3926 0
|
缓存 固态存储 分布式数据库
hbase源码系列(十三)缓存机制MemStore与Block Cache
这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存。
2882 0
|
分布式数据库 Hbase
hbase源码系列(十二)Get、Scan在服务端是如何处理?
继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以走这个过程,所以就一起写了。
3379 0
|
分布式数据库 Hbase
hbase源码系列(十一)Put、Delete在服务端是如何处理?
在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?
2066 0
|
分布式数据库 Hbase
hbase源码系列(十)HLog与日志恢复
hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢复。本文将介绍“HLog与日志恢复”。
1983 0
|
存储 分布式数据库 索引
hbase源码系列(九)StoreFile存储格式
从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去,这块是基础,Region Sever上面的操作,大部分都是基于它来进行的。
3018 0