论文地址

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数组中找到查找到的节点的子节点,即

Node findChild(int value){
for(int i=0;i<4;i++){
if(key[i] == value) return childPointer[i];
}
return null;
}

Node16

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

Node48

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

Node findChild(int value){
return childPointer[childIndex[value]];
}

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

Node256

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

Node findChild(int value){
return childPointer[value];
}

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

成员变量

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

方法

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

Leaf.java

成员变量

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

方法

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

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

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

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

@Override public boolean insert()插入操作

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

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

@Override public boolean insert(ChildPtr ref, final byte[] key, Object value,
int depth, boolean force_clone) throws UnsupportedOperationException {
boolean clone = force_clone || this.refcount > 1;// 即论文中的是 path-copy or in-place update
if (matches(key)) { // 匹配到存在叶子结点 即更新旧节点
if (clone) { // path copy
// Updating an existing value, but need to create a new leaf to
// reflect the change
ref.change(new Leaf(key, value));
} else {// in-place update
// Updating an existing value, and safe to make the change in
// place
this.value = value;
}
return false;
} else { // 插入叶节点
// New value
// Create a new leaf
Leaf l2 = new Leaf(key, value);
// Determine longest prefix
int longest_prefix = longest_common_prefix(l2, depth);
if (depth + longest_prefix >= this.key.length ||
depth + longest_prefix >= key.length) {
throw new UnsupportedOperationException("keys cannot be prefixes of other keys");
}
// Split the current leaf into a node4
ArtNode4 result = new ArtNode4();
result.partial_len = longest_prefix;
Node ref_old = ref.get(); //旧的指向该叶节点的内部节点
ref.change_no_decrement(result);// 直接更新
System.arraycopy(key, depth,
result.partial, 0,
Math.min(Node.MAX_PREFIX_LEN, longest_prefix));
// Add the leafs to the new node4
result.add_child(ref, this.key[depth + longest_prefix], this);
result.add_child(ref, l2.key[depth + longest_prefix], l2);
ref_old.decrement_refcount();// 原来的节点由叶节点变成了内部节点 因此原来节点
// TODO: avoid the increment to self immediately followed by decrement
return true;
}
}

ArtNode.java

成员变量

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

方法

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

public int prefix_mismatch(final byte[] key, int depth) {
int max_cmp = Math.min(Math.min(Node.MAX_PREFIX_LEN, partial_len), key.length - depth);
int idx;
for (idx = 0; idx < max_cmp; idx++) {
if (partial[idx] != key[depth + idx])
return idx;
}
// If the prefix is short we can avoid finding a leaf
if (partial_len > Node.MAX_PREFIX_LEN) {
// Prefix is longer than what we've checked, find a leaf
final Leaf l = this.minimum();
max_cmp = Math.min(l.key.length, key.length) - depth;
for (; idx < max_cmp; idx++) {
if (l.key[idx + depth] != key[depth + idx])
return idx;
}
}
return idx;
}

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

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

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

delete()删除操作

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

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

ArtNode4.java

成员变量

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

方法

add_child()增加一个子节点

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

remove_child()移除一个子节点

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

@Override public void remove_child(ChildPtr ref, byte c) {
assert(refcount <= 1);
int idx;
for (idx = 0; idx < this.num_children; idx++) {
if (c == keys[idx]) break;
}
if (idx == this.num_children) return;
assert(children[idx] instanceof Leaf);
children[idx].decrement_refcount();
// Shift to fill the hole
System.arraycopy(this.keys, idx + 1, this.keys, idx, this.num_children - idx - 1);
System.arraycopy(this.children, idx + 1, this.children, idx, this.num_children - idx - 1);
this.num_children--;
// Remove nodes with only a single child
if (num_children == 1) {
Node child = children[0];
if (!(child instanceof Leaf)) {
if (((ArtNode)child).refcount > 1) {
child = child.n_clone();
}
ArtNode an_child = (ArtNode)child;
// Concatenate the prefixes
int prefix = partial_len;
if (prefix < MAX_PREFIX_LEN) {
partial[prefix] = keys[0];
prefix++;
}
if (prefix < MAX_PREFIX_LEN) {
int sub_prefix = Math.min(an_child.partial_len, MAX_PREFIX_LEN - prefix);
System.arraycopy(an_child.partial, 0, partial, prefix, sub_prefix);
prefix += sub_prefix;
}
// Store the prefix in the child
System.arraycopy(partial, 0, an_child.partial, 0, Math.min(prefix, MAX_PREFIX_LEN));
an_child.partial_len += partial_len + 1;
}
ref.change(child);
}
}

对于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. js前端加密,php后端解密(crypto-js,openssl_decrypt)

    来源:https://blog.csdn.net/morninghapppy/article/details/79044026 案例:https://blog.csdn.net/zhihua_w/ar ...

  2. Winsock select server 与 client 示例代码

    参考 https://www.winsocketdotnetworkprogramming.com/winsock2programming/winsock2advancediomethod5.html ...

  3. [Inno Setup] 对比字符串

    [Code] var MD5Comp: string; procedure ExitProcess(uExitCode:UINT); external 'ExitProcess@kernel32.dl ...

  4. ELK6.3版本安装部署

    一.Elasticsearch 安装 1.部署系统以及环境准备 cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core) uname - ...

  5. 动画图解Git命令

    ​Git是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常大的项目版本管理,是目前使用范围最广的版本管理工具 尽管Git是一个非常强大的工具,但我认为大多数人都会同意我的说法,即它也可以 ...

  6. java中Locks的使用

    文章目录 Lock和Synchronized Block的区别 Lock interface ReentrantLock ReentrantReadWriteLock StampedLock Cond ...

  7. java 中的fork join框架

    文章目录 ForkJoinPool ForkJoinWorkerThread ForkJoinTask 在ForkJoinPool中提交Task java 中的fork join框架 fork joi ...

  8. 使用 html5 FileReader 获取图片, 并异步上传到服务器 (不使用 iframe)

    为什么80%的码农都做不了架构师?>>>   原理: 1.使用FileReader 读取图片的base64编码 2.使用ajax,把图片的base64编码post到服务器. 3.根据 ...

  9. Linux笔记(shell基础,历史命令,命令补全/别名,通配符,输出重定向)

    一.shell 基础 shell是个命令解释器,提供用户和机器之间的交互 每个用户都可以拥有自己特定的shell centos7默认Shell为bash(Bourne Agin shell) 除了ba ...

  10. CodeForces - 1176A Divide it! (模拟+分类处理)

    You are given an integer nn. You can perform any of the following operations with this number an arb ...