4个版本 (2个稳定版本)

新增 2.0.0 2024年8月24日
1.0.0 2024年7月28日
1.0.0-rc.22024年7月12日
1.0.0-rc.12023年6月26日

#649数据库接口

Download history 5/week @ 2024-05-19 1/week @ 2024-05-26 3/week @ 2024-06-09 1/week @ 2024-06-16 3/week @ 2024-06-23 7/week @ 2024-06-30 106/week @ 2024-07-07 40/week @ 2024-07-14 23/week @ 2024-07-21 272/week @ 2024-07-28 72/week @ 2024-08-04 45/week @ 2024-08-11

每月416次下载
用于 3 crates

MIT 许可证

51KB
968

GroveDB

分支 测试 覆盖率
master Tests codecov

高效二级索引查询的分层认证数据结构

GroveDB是一种专为高效二级索引查询、证明、速度和可靠性设计的数据库系统。它为Dash平台的使用而构建,但可以轻松集成到其他类似用途的应用程序中。

动机

二级索引对于任何数据库管理系统都至关重要。所有先前的解决方案都根据它们试图解决的问题有一定的权衡。

考虑一个认证数据结构,例如基于餐厅数据库构建的Merkle树。每个餐厅都有某些属性,例如价格和类型

struct Restaurant{
	ID uint32;
	name: String;
	type: String;
	isVegan: bool;
};

如果我们有四家餐厅,我们可能会将它们提交给Merkle树,如下所示

graph TD;
root-->A[" "];
root-->B[" "];
A-->AA["id:0"];
A-->AB["id:1"];
B-->AC["id:2"];
B-->AD["id:3"];

通过主键查询既简单又高效。如果我们有一个查询,如SELECT * WHERE ID <= 2; ,我们可以返回适当的元素并构建一个有效的范围证明。但是,通过二级索引查询效率非常低;你很可能会遍历整个结构。考虑查询 SELECT * WHERE isVegan=true;。当按主键排序时,素食餐厅不会连续。不仅证明将变得复杂,而且查找这些元素所需的时间也将变得很长。

GroveDB 是一种经典的时间空间权衡。它通过预计算和提交来支持对辅助索引的高效查询。每个可能的查询辅助索引的子树(最多到一定限制)被构建并提交到我们的认证数据结构中。子树的树;一个森林。对于相同的数据,类似的 GroveDB 结构可能如下所示:

graph TD;
root-->A["\'Restaurant\'"];
root-->B["..."];
A-->Q["ID"];
A-->W["name"];
A-->E["kind"];
A-->R["isVegan"];
Q-->Z["..."];
W-->X["..."];
E-->C["..."];
R-->Y["id:2"];
R-->U["id:1"];
R-->I["id:0"];
R-->O["id:3"];

从这里,对辅助索引 isVegan 的查询将遍历为该辅助索引构建的子树。项目不一定是复制的,而是引用的。

特性

  • 高效的辅助索引查询 - 专门为辅助索引查询构建和定制。
  • 证明 - 支持成员资格证明、非成员资格证明和范围证明。
  • 运行在任何地方 - 由于使用 Rust 编写,它支持所有编译目标。x86、树莓派(AArch64)和 Wasm。还有 Node.js 绑定。

架构

插入和删除操作与预期一致,更新相应的子树,并返回适当的成员资格/非成员资格证明。

树结构

我们选择一个统一的;一个基于使用分层认证数据结构的数据库外包的分层、认证数据结构。元素是最原子的部分,可以用几种方式表示。它们可以是项、项引用、树、带有项的树,甚至是带有项引用的树。一个元素包含一个项、一个对象的引用或一个子树。

这些树是基于我们对 Merk 的分支,并针对与 GroveDB 的更好的使用应用了自定义补丁。Merk 的独特之处在于它是一个 AVL 树,所以中间节点也包含一个键/值对。每个节点包含一个第三个哈希,即 kv_hash,除了其左右子节点的哈希之外。 kv_hash 简单计算为 kv_hash=H(key,value)。节点哈希随后计算为 H(kv_hash,left_child_hash,right_child_hash)。Merk 使用 Blake2B,而 rs-merkle 使用 SHA256。

存储

RocksDB 是一个键值存储,由 LevelDB 分支而来,由 Facebook 构建。我们选择它是因为其高性能、成熟度和与我们的堆栈的兼容性。Merk 本身是建立在 RocksDB 之上的。

我们有三种类型的存储:辅助存储、元数据和树根存储。辅助存储用于存储不用于共识的纯键值数据。元数据用于存储 GroveDB 使用范围之外的事物。它没有前缀,因此与子树无关。它位于更高层次。树根存储用于存储子树。

GroveDB 中的数据库事务是围绕 RocksDB 的 OptimisticTransactionDB 原语的一个包装。乐观事务希望平均只有少数冲突,这些冲突在提交阶段被检测到。这与使用锁的悲观模型相比。

查询

要查询 GroveDB,必须提供路径和查询项。路径指定子树,而查询项确定从子树中选择哪些节点。

GroveDB 目前支持 10 种查询项类型

  • 键(键名)
  • 范围(开始..结束)
  • 范围(开始..=结束)
  • 范围(..)
  • 范围(开始..)
  • 范围(..结束)
  • 范围(..=结束)
  • 范围(前一个..)
  • 范围(前一个..结束)
  • 范围(前一个..=结束)

本部分描述了一个基本的查询系统:选择一个子树,然后从该子树中选择节点。可能会出现创建更复杂的查询或对结果集添加限制的需求,这导致我们引入了 PathQuery

PathQuery

PathQuery 允许进行更复杂的查询,并且可以对结果集进行可选的限制,即限制和偏移量。

    PathQuery
        path: [k1, k2, ..]
        sized_query: SizedQuery
            limit: Optional<number>
            offset: Optional<number>
            query: Query
                items: [query_item_1, query_item_2, ...],
                default_subquery_branch: SubqueryBranch
                    subquery_path: Optional<key>
                    subquery_value: Optional<Query>
                conditional_subquery_branches: Map<QueryItem, SubqueryBranch>
                        

需要一个路径来定义查询的起始上下文。

SizedQuery

sized_query 决定了结果集如何被限制。它包含可选的限制和偏移量值。 limit 决定结果集的最大大小,而 offset 指定在添加到结果集之前要跳过的元素数量。

Query

query 对象是一个递归结构 - 它指定如何从当前子树中选择节点,并且可以选择将另一个查询递归地应用于上一个查询得到的结果集。

Items

items 是一组查询项,用于决定从当前上下文中选择哪些节点(这构建了一个结果集)。

在描述 default_subquery_branchconditional_subquery_branches 之前,我们需要定义它们的构建块,即子查询分支。

子查询分支

    subquery_path: Optional<Key>
    subquery_value: Optional<Query>

情况

  • subquery_path: truesubquery_value: false
    选择具有子查询路径的节点,并将其作为结果集返回。

  • subquery_path: falsesubquery_value: true
    将存储在子查询值中的查询直接应用于子树,并将结果作为结果集返回。

  • subquery_path: truesubquery_value: true 首先选择具有子查询路径的节点,并将其设置为新的上下文。
    然后,将子查询值应用于这个新上下文,并将结果作为结果集返回。

子查询分支用于单个节点,但可以使用 default_subquery_branchconditional_subquery_branches 应用于先前查询的结果集。

default_subquery_branch

如果存在,则将指定的子查询分支应用于先前查询的结果集中的每个节点。

conditional_subquery_branch

而不是将子查询分支应用于结果集中的每个节点,您可能只想将其应用于结果集的一个子集。在这种情况下,我们可以使用条件子查询。

条件子查询包含一个映射,将 QueryItem 映射到 SubqueryBranch。

    Map<QueryItem, SubqueryBranch>

对于结果集中的每个节点,我们检查是否存在匹配的查询项。如果存在,则将关联的子查询分支应用于该节点。注意,一旦对节点应用了条件子查询,则默认子查询将不会运行。

合并路径查询

本节描述 GroveDB 如何处理路径查询的合并。

可合并路径查询允许将单独的路径查询组合成一个等价的单个路径查询。

一个路径查询可以表示为一个键的集合(路径到一个子树),以及应用于该子树的查询(查询可以具有未知深度)。

pi = [k1, k2, ..., kn, Query]

以下要展示的是非常重要的一点,即路径查询链可以在任何位置进行压缩,也就是说,你可以将一系列键转换为一个单一查询。

考虑 p1 = [k1, k2, k3]。这表示

  • 从根树中,选择键为 k1 的节点
  • 将上下文更改为 k1,然后选择键为 k2 的节点
  • 将上下文更改为 k2,最后选择键为 k3 的节点

我们可以创建一个等效查询来表示这一点,其可能看起来如下

    Query
        query k1
        cond on k1
            query k2
            cond on k2
                query k3
                cond on k3

[k1, k2, k3] => Q1,其中 Q1 等价于路径数组。

这也可以在路径数组中的任何位置进行,因此我们可以有

[k1, k2, k3] => [k1, Q2]
[k1, k2, k3] => [K1, K2 Q3]

路径合并算法如下

  • 找到路径查询中的公共路径
  • 在公共路径索引之后压缩每个路径数组
  • 将压缩后的查询合并为单个查询
  • 返回新的路径查询,其中公共路径作为路径,组合查询作为查询

示例
p1 = [k1, k2, k3, Qa]
p2 = [k1, k2, k4, Qb]

公共路径 = [k1, k2]

压缩公共路径之后的每个路径数组
p1 = [k1, k2, Qc]
p2 = [k1, k2, Qd]

合并压缩后的查询
Qp = Qc + Qd

返回最终的 PathQuery
pf = [k1, k2, Qp]

使用

GroveDB是为与 Dash 平台一起使用而构建的,但可以轻松集成到其他应用程序中进行类似的使用。请参阅其在 rs-drive (示例) 中的使用。

我们目前也提供了 Node.js 的绑定。请参阅 node-grove

构建

首先,使用您首选的方法安装 rustup

构建需要 Rust 夜间版本,因此请确保您使用的是正确的版本。

rustup安装 nightly

克隆存储库并导航到主目录

gitclone https://github.com/dashevo/grovedb.git && cdgrovedb

从这里我们可以构建

cargobuild

grovedbg

有一个正在进行的 GroveDB 调试层实现。要使用此库并启用这些功能,需要设置带有 grovedbg 功能的依赖项。

然后,要启动可视化工具以在浏览器中的端口上观察数据库结构,例如在端口 10000 上,以下代码片段应该可以做到

    let db = Arc::new(GroveDb::open("db").unwrap());
    db.start_visualzier(10000);

只需记住使用 Arc,因为 HTTP 服务器可能比 GroveDB 实例存在的时间更长。

性能

使用 cargo test

CPU 时间
Raspberry Pi 4 2m58.491s
R5 1600AF 33.958s
R5 3600 25.658s

lib.rs:

接口包,用于统一操作成本的传递和检索方式。

依赖项

~320–770KB
~18K SLoC