论文地址

Adaptive Radix Tree: https://db.in.tum.de/~leis/papers/ART.pdf

Persistent Adaptive Radix Tree: https://ankurdave.com/dl/part-tr.pdf

代码地址: https://github.com/ankurdave/part

数据结构

如图所示 为整颗树的大致结构 分为根节点root 普通节点node和叶节点leaf。整个查找过程从根节点开始找到叶节点,例如第一个叶节点的查找过程即为A-N-D,最后找到AND叶节点。有时叶节点也会指向另一个值,这样我们就构成了一个很方便查找的key-value键值对结构。Persistent Adaptive Radix Tree基本是基于Radix tree进行的改动,网上有很多文章阐述Radix Tree,本文不再赘述。

Node4

Node4是PART中最小的一种node结构,可以存储4个子节点指针,通常用于子节点数目为1-4个的node节点。这个结构由key数组和child pointer数组组成,且key的顺序和child pointer的顺序是相应的。查找具体节点的过程可以在key数组中顺序或二分查找,查找到的下标可在child pointer数组中找到查找到的节点的子节点,即

  1. Node findChild(int value){
  2. for(int i=0;i<4;i++){
  3. if(key[i] == value) return childPointer[i];
  4. }
  5. return null;
  6. }

Node16

Node16存储5-16个子节点,具体细节与Node4基本一致 不再赘述

Node48

Node48开始就有所不同了,可以存储17-48个子节点。其用到长度为256的子索引数组和48的child pointer数组。由于此时包含的子节点过多,线性查找效率低下,于是就采用直接映射的方式。例如当前的要查找的value是123,那么就在child index中取childIndex[123],其值就是child pointer中的下标,childPointer[childIndex[123]]就是对应的子节点,即

  1. Node findChild(int value){
  2. return childPointer[childIndex[value]];
  3. }

由于child index中最大值为48,因此6 bits就足够(有时出于方便使用1byte)相比直接使用256个child pointer(2568=2048),两者结合(2561+48*8=640)的方式更加节省空间。

Node256

存储49-256个子节点,采用直接映射的方式

  1. Node findChild(int value){
  2. return childPointer[value];
  3. }

Path Compression和Lazy Expansion

lazy expansion是指内部节点node只有在用来区分两个指向不同叶节点的路径时才会被创建(通俗来讲,含有前缀时)。如图所示,当只有一条路径指向FOO时,那么仅有FOO一个叶节点而没有中间的两个OO;当另一个以F开头的叶节点插入时候,此时就需要拓展出OO来和另一个F开头的叶节点做区分。

path compression是指当只有一个子节点时,移除所有的内部节点node(通常是合并入子节点)。合并带来了前缀,前缀需要在查找叶节点时时进行比较,于是有两种方法解决这个问题:

  1. 悲观方法:在每个内部节点上,都存储了一个可变长度(可能为空)的部分key的vector。 它包含所有先前已删除的单一节点的key。 在查找期间,将此vector与搜索关键字value进行比较,然后继续处理下一个子节点。
  2. 乐观方法:仅存储先前的单一节点的数量(等于悲观方法中向量的长度)。 查找只是跳过此字节数,而不进行比较。 因此,当查找到达叶子时,必须将其关键字与搜索关键字进行比较,以确保未进行“错误的转弯”。

在 PART 的实现中结合了这两种方法,每个节点存放最多8个字节的前缀,下降会根据前缀长度进行动态切换。

其他特性

为了持久化 还是用了Path Copying 即更新时拷贝待更新节点至根节点之间的路径 返回一个新的变更后的节点。 对于非交集节点还可以使用直接更新的方式加快速度,如图5

为了解决碎片问题和分布过散的问题,本文还提出了池化和删除后压缩的方法。如图6

本文使用了检查点机制Incremental Checkingpointing保证错误恢复。如果一个子树当前时刻与上一个检查点完全一致,就直接refer到上一个检查点而不写出。具体做法是:对当前状态做一个快照,分隔为子树,将每一个子树存为一个不同的文件,将页间指针作为文件标识符。根节点指向每个子树的文件标识符,并且每次的直接更新都会删除文件描述符,这样每个子树都可以保证被更新到最新版本而不会被下一次checkingpointing重复。

源码剖析

项目结构

类图

以下内容请结合代码https://github.com/ankurdave/part

Node.java

成员变量

  1. static final int MAX_PREFIX_LEN = 8;// 上文提到的copy compression时的最长前缀长度
  2. int refcount;// 被引用数 类比垃圾处理机制的引用计数器

方法

此类是一个抽象类 定义了一些节点应有的属性 具体实现参加具体的节点

Leaf.java

成员变量

  1. public static int count;// 叶子节点数
  2. Object value;// 值
  3. final byte[] key;// 键 注意这里的键是下降查找过程中所有的键 即原始的(k,v)的k

方法

prefix_matches(final byte[] prefix)用于验证前缀是否和该叶节点的key匹配。注意prefix的长度一定不超过key的长度

  1. public boolean prefix_matches(final byte[] prefix) { //prefix是指产生了节点压缩之后的前缀
  2. if (this.key.length < prefix.length) return false;
  3. for (int i = 0; i < prefix.length; i++) {
  4. if (this.key[i] != prefix[i]) {
  5. return false;
  6. }
  7. }
  8. return true;
  9. }

longest_common_prefix(Leaf other, int depth)比较两个叶节点的最长公共前缀。这里的最长公共前缀并非是从key的0位置算起的,而是从depth(当前节点深度)开始算。该方法的用处是在insert方法中得到一个longest_prefix来将当前的节点分裂成一个ArtNode4,故而所需的前缀只是从当前节点深度对应的key开始算起的。

  1. public int longest_common_prefix(Leaf other, int depth) { // 从depth开始算 最长公共前缀 这里的depth应该是当前下降的深度
  2. int max_cmp = Math.min(key.length, other.key.length) - depth;
  3. int idx;
  4. for (idx = 0; idx < max_cmp; idx++) {
  5. if (key[depth + idx] != other.key[depth + idx]) {
  6. return idx;
  7. }
  8. }
  9. return idx;
  10. }

@Override public boolean insert()插入操作

如图,想要把(...FOA,2)插入到树中,插入结果如右边所示。插入有两种情况:

  1. key已经存在,那么直接更新叶子节点即可。
  2. key不存在。注意到我们实现的是Leaf类的insert 即从根节点下降查找的过程中 下降到最后遇到的是叶子节点。一旦出现这种情况,我们需要记起之前所提到的lazy expansion,因此我们需要获取待插入叶节点和当前叶节点的公共前缀,并将当前叶节点变成内部节点ArtNode4,并将ArtNode4指向当前节点和待插入节点。示例如图:

  1. @Override public boolean insert(ChildPtr ref, final byte[] key, Object value,
  2. int depth, boolean force_clone) throws UnsupportedOperationException {
  3. boolean clone = force_clone || this.refcount > 1;// 即论文中的是 path-copy or in-place update
  4. if (matches(key)) { // 匹配到存在叶子结点 即更新旧节点
  5. if (clone) { // path copy
  6. // Updating an existing value, but need to create a new leaf to
  7. // reflect the change
  8. ref.change(new Leaf(key, value));
  9. } else {// in-place update
  10. // Updating an existing value, and safe to make the change in
  11. // place
  12. this.value = value;
  13. }
  14. return false;
  15. } else { // 插入叶节点
  16. // New value
  17. // Create a new leaf
  18. Leaf l2 = new Leaf(key, value);
  19. // Determine longest prefix
  20. int longest_prefix = longest_common_prefix(l2, depth);
  21. if (depth + longest_prefix >= this.key.length ||
  22. depth + longest_prefix >= key.length) {
  23. throw new UnsupportedOperationException("keys cannot be prefixes of other keys");
  24. }
  25. // Split the current leaf into a node4
  26. ArtNode4 result = new ArtNode4();
  27. result.partial_len = longest_prefix;
  28. Node ref_old = ref.get(); //旧的指向该叶节点的内部节点
  29. ref.change_no_decrement(result);// 直接更新
  30. System.arraycopy(key, depth,
  31. result.partial, 0,
  32. Math.min(Node.MAX_PREFIX_LEN, longest_prefix));
  33. // Add the leafs to the new node4
  34. result.add_child(ref, this.key[depth + longest_prefix], this);
  35. result.add_child(ref, l2.key[depth + longest_prefix], l2);
  36. ref_old.decrement_refcount();// 原来的节点由叶节点变成了内部节点 因此原来节点
  37. // TODO: avoid the increment to self immediately followed by decrement
  38. return true;
  39. }
  40. }

ArtNode.java

成员变量

  1. int num_children = 0;
  2. int partial_len = 0;// path compression时的前缀长度
  3. final byte[] partial = new byte[Node.MAX_PREFIX_LEN]; // path compression时的前缀

方法

prefix_mismatch()查找key和当前ArtNode最先不匹配的位置。由于在path compression时候 我们使用了乐观+悲观的方式,因此前缀长度大于我们规定的上限8时,多出来的前缀溢出存储到其子节点中。

  1. public int prefix_mismatch(final byte[] key, int depth) {
  2. int max_cmp = Math.min(Math.min(Node.MAX_PREFIX_LEN, partial_len), key.length - depth);
  3. int idx;
  4. for (idx = 0; idx < max_cmp; idx++) {
  5. if (partial[idx] != key[depth + idx])
  6. return idx;
  7. }
  8. // If the prefix is short we can avoid finding a leaf
  9. if (partial_len > Node.MAX_PREFIX_LEN) {
  10. // Prefix is longer than what we've checked, find a leaf
  11. final Leaf l = this.minimum();
  12. max_cmp = Math.min(l.key.length, key.length) - depth;
  13. for (; idx < max_cmp; idx++) {
  14. if (l.key[idx + depth] != key[depth + idx])
  15. return idx;
  16. }
  17. }
  18. return idx;
  19. }

insert(),即下降查找到最后是一个ArtNode时,插入一个叶子节点。

  • 如果该ArtNode有前缀,即进行过path compression

    • if不一致发生在前缀长度之后 那么depth增加partial_len,去找叶子节点
    • else 分裂当前节点 生成新节点,令公共前缀为其前缀,公共前缀后一字节作为区分两个 key 的字节,然后将叶子节点和截断公共前缀后的老节点插入到这个新节点中
  • 没有前缀或不一致发生在前缀长度之后 如果能获取到子节点 则在子节点中插入;否则 在本节点插入
  1. @Override public boolean insert(ChildPtr ref, final byte[] key, Object value,
  2. int depth, boolean force_clone) {
  3. boolean do_clone = force_clone || this.refcount > 1;
  4. // Check if given node has a prefix
  5. if (partial_len > 0) {
  6. // Determine if the prefixes differ, since we need to split
  7. int prefix_diff = prefix_mismatch(key, depth);
  8. if (prefix_diff >= partial_len) {
  9. depth += partial_len; // 如果不一致的地方在partial后 那么则partial中的全都被匹配上了 去找叶子 depth增加partial_len
  10. } else {
  11. // Create a new node
  12. ArtNode4 result = new ArtNode4();
  13. Node ref_old = ref.get();
  14. // ref被一个新节点result共享
  15. ref.change_no_decrement(result); // don't decrement yet, because doing so might destroy self
  16. result.partial_len = prefix_diff;
  17. System.arraycopy(partial, 0,
  18. result.partial, 0,
  19. Math.min(Node.MAX_PREFIX_LEN, prefix_diff));
  20. // Adjust the prefix of the old node
  21. ArtNode this_writable = do_clone ? (ArtNode)this.n_clone() : this;
  22. if (partial_len <= Node.MAX_PREFIX_LEN) {
  23. result.add_child(ref, this_writable.partial[prefix_diff], this_writable);
  24. this_writable.partial_len -= (prefix_diff + 1);
  25. System.arraycopy(this_writable.partial, prefix_diff + 1,
  26. this_writable.partial, 0,
  27. Math.min(Node.MAX_PREFIX_LEN, this_writable.partial_len));
  28. } else {
  29. this_writable.partial_len -= (prefix_diff+1);
  30. final Leaf l = this.minimum();
  31. result.add_child(ref, l.key[depth + prefix_diff], this_writable);
  32. System.arraycopy(l.key, depth + prefix_diff + 1,
  33. this_writable.partial, 0,
  34. Math.min(Node.MAX_PREFIX_LEN, this_writable.partial_len));
  35. }
  36. // Insert the new leaf
  37. Leaf l = new Leaf(key, value);
  38. result.add_child(ref, key[depth + prefix_diff], l);
  39. ref_old.decrement_refcount();
  40. return true;
  41. }
  42. }

delete()删除操作

  • 如果key在当前node没有匹配 那么不存在节点 退出

    • 深度增加一个前缀长度
  • 查找子节点
    • 没找到 错误 退出
    • 删除叶子节点本身 并 remove child
  1. @Override public boolean delete(ChildPtr ref, final byte[] key, int depth,
  2. boolean force_clone) {
  3. // Bail if the prefix does not match
  4. if (partial_len > 0) {
  5. int prefix_len = check_prefix(key, depth);
  6. if (prefix_len != Math.min(MAX_PREFIX_LEN, partial_len)) {
  7. return false;
  8. }
  9. depth += partial_len;
  10. }
  11. boolean do_clone = force_clone || this.refcount > 1;
  12. // Clone self if necessary. Note: this allocation will be wasted if the
  13. // key does not exist in the child's subtree
  14. ArtNode this_writable = do_clone ? (ArtNode)this.n_clone() : this;
  15. // Find child node
  16. ChildPtr child = this_writable.find_child(key[depth]);
  17. if (child == null) return false; // when translating to C++, make sure to delete this_writable
  18. if (do_clone) {
  19. ref.change(this_writable);
  20. }
  21. boolean child_is_leaf = child.get() instanceof Leaf;
  22. boolean do_delete = child.get().delete(child, key, depth + 1, do_clone);
  23. if (do_delete && child_is_leaf) {
  24. // The leaf to delete is our child, so we must remove it
  25. this_writable.remove_child(ref, key[depth]);
  26. }
  27. return do_delete;
  28. }

ArtNode4.java

成员变量

  1. public static int count;// ArtNode4节点数目
  2. byte[] keys = new byte[4];
  3. Node[] children = new Node[4];

方法

add_child()增加一个子节点

  • 首先检查子节点数是若没超过4个 则找到key待拆入位置(key是增序的) 然后插入
  • 否则变更为ArtNode16再在增加
  1. @Override public void add_child(ChildPtr ref, byte c, Node child) {
  2. assert(refcount <= 1);
  3. if (this.num_children < 4) {
  4. int idx;
  5. for (idx = 0; idx < this.num_children; idx++) {
  6. if (to_uint(c) < to_uint(keys[idx])) break;
  7. }
  8. // Shift to make room
  9. System.arraycopy(this.keys, idx, this.keys, idx + 1, this.num_children - idx);
  10. System.arraycopy(this.children, idx, this.children, idx + 1, this.num_children - idx);
  11. // Insert element
  12. this.keys[idx] = c;
  13. this.children[idx] = child;
  14. child.refcount++;
  15. this.num_children++;
  16. } else {
  17. // Copy the node4 into a new node16
  18. ArtNode16 result = new ArtNode16(this);
  19. // Update the parent pointer to the node16
  20. ref.change(result);
  21. // Insert the element into the node16 instead
  22. result.add_child(ref, c, child);
  23. }
  24. }

remove_child()移除一个子节点

这里需要注意的时如果移除后,仅剩一个子节点且不为叶子节点,那么就会发生path compression。这里将本节点的唯一一个key移入partial,然后合并到子节点。

  1. @Override public void remove_child(ChildPtr ref, byte c) {
  2. assert(refcount <= 1);
  3. int idx;
  4. for (idx = 0; idx < this.num_children; idx++) {
  5. if (c == keys[idx]) break;
  6. }
  7. if (idx == this.num_children) return;
  8. assert(children[idx] instanceof Leaf);
  9. children[idx].decrement_refcount();
  10. // Shift to fill the hole
  11. System.arraycopy(this.keys, idx + 1, this.keys, idx, this.num_children - idx - 1);
  12. System.arraycopy(this.children, idx + 1, this.children, idx, this.num_children - idx - 1);
  13. this.num_children--;
  14. // Remove nodes with only a single child
  15. if (num_children == 1) {
  16. Node child = children[0];
  17. if (!(child instanceof Leaf)) {
  18. if (((ArtNode)child).refcount > 1) {
  19. child = child.n_clone();
  20. }
  21. ArtNode an_child = (ArtNode)child;
  22. // Concatenate the prefixes
  23. int prefix = partial_len;
  24. if (prefix < MAX_PREFIX_LEN) {
  25. partial[prefix] = keys[0];
  26. prefix++;
  27. }
  28. if (prefix < MAX_PREFIX_LEN) {
  29. int sub_prefix = Math.min(an_child.partial_len, MAX_PREFIX_LEN - prefix);
  30. System.arraycopy(an_child.partial, 0, partial, prefix, sub_prefix);
  31. prefix += sub_prefix;
  32. }
  33. // Store the prefix in the child
  34. System.arraycopy(partial, 0, an_child.partial, 0, Math.min(prefix, MAX_PREFIX_LEN));
  35. an_child.partial_len += partial_len + 1;
  36. }
  37. ref.change(child);
  38. }
  39. }

对于ArtNode16.java ArtNode48.java和ArtNode256.java,实现方式与ArtNode4.java大致相似,具体差异课参考上一节的数据结构部分来理解。其余文件,均为一些基础性代码,例如迭代器等,非常好理解,此处不再赘述。

PART(Persistent Adaptive Radix Tree)的Java实现源码剖析的更多相关文章

  1. Java ArrayList源码剖析

    转自: Java ArrayList源码剖析 总体介绍 ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现.除该类未实现同步外 ...

  2. 转:【Java集合源码剖析】LinkedHashmap源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/37867985   前言:有网友建议分析下LinkedHashMap的源码,于是花了一晚上时 ...

  3. 转:【Java集合源码剖析】TreeMap源码剖析

    前言 本文不打算延续前几篇的风格(对所有的源码加入注释),因为要理解透TreeMap的所有源码,对博主来说,确实需要耗费大量的时间和经历,目前看来不大可能有这么多时间的投入,故这里意在通过于阅读源码对 ...

  4. 转:【Java集合源码剖析】Hashtable源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元 ...

  5. 转:【Java集合源码剖析】HashMap源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955   您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票 ...

  6. 转:【Java集合源码剖析】Vector源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/35793865   Vector简介 Vector也是基于数组实现的,是一个动态数组,其容量 ...

  7. 转:【Java集合源码剖析】LinkedList源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/35787253   您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票 ...

  8. 转:【Java集合源码剖析】ArrayList源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/35568011   本篇博文参加了CSDN博文大赛,如果您觉得这篇博文不错,希望您能帮我投一 ...

  9. 【Java集合源码剖析】Hashtable源码剖析

    转载出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一 ...

随机推荐

  1. php class 访问控制

    属性(attribute ) 必须声明访问控制类型 类型: public 公用 protected 受保护的 private  私有的 public 类型的属性 可以在外部访问 protected 及 ...

  2. Inno Setup: Ask for reboot after uninstall

     https://stackoverflow.com/questions/36497580/inno-setup-ask-for-reboot-after-uninstall   Use Uninst ...

  3. 理解async/await

    async 和 await 在干什么 任意一个名称都是有意义的,先从字面意思来理解.async 是“异步”的简写,而 await 可以认为是 async wait 的简写.所以应该很好理解 async ...

  4. Linux 用户管理和提权

    Linux ⽀持多个⼈使⽤同⼀个⽤户登录系统, Windows 在修改组策略的情况下,也可以多个⼈使⽤同⼀个⽤户登录 远程连接Linux的⽅式:SSH协议 远程连接Windows的⽅式:RDP协议 安 ...

  5. Docker容器利用weave实现跨主机互联

    Docker容器利用weave实现跨主机互联 环境: 实现目的:实现主机A中容器1与主机B中容器1的网络互联 主机A步骤: ①下载复制weave二进制执行文件(需要internet)[root@192 ...

  6. [UWP]推荐一款很Fluent Design的bilibili UWP客户端 : 哔哩

    UWP已经有好几个Bilibili的客户端,最近有多了一个: 哔哩 - Microsoft Store 作者云之幻是一位很擅长设计的UWP开发者,我也从他那里学到了很多设计方面的技巧.它还是一位Bil ...

  7. 【大数据 Spark】利用电影观看记录数据,进行电影推荐

    利用电影观看记录数据,进行电影推荐. 目录 利用电影观看记录数据,进行电影推荐. 准备 1.任务描述: 2.数据下载 3.部分数据展示 实操 1.设置输入输出路径 2.配置spark 3.读取Rati ...

  8. vue滑动吸顶以及锚点定位

    Vue项目中需要实现滑动吸顶以及锚点定位功能.template代码如下: <template> <div class="main"> <div id= ...

  9. 【FreeRTOS实战汇总】小白博主的RTOS学习实战快速进阶之路(持续更新)

    博主是个小白,打算把这段时间系统学习RTOS的文章统一整理到这里,另外本文会给出一些参考性资料和指导性建议: 本文宗旨 FreeRTOS 是由Richard Barry在2003年由设计的,由于其设计 ...

  10. java知识点查漏补缺-- 2020512

    jvm: jdbc statement: JDBC statement中的PReparedStatement的占位符对应着即将与之对应当值,并且一个占位符只能对应一个值,如果能对应多个就会引起混淆.s ...