将 Oracle Berkeley DB 用作 NoSQL 数据存储

作者:Shashank Tiwari

了解 Oracle Berkeley DB 可以为您的应用程序带来 NoSQL 优势的原因及方式。

2011 年 2 月发布

“NoSQL”是在开发人员、架构师甚至技术经理中新流行的一个词汇。尽管这个术语最近很流行,但令人惊讶的是,它并没有一个普遍认可的定义。

通常来说,任何非 RDBMS 且遵循无模式结构的数据库一般都不能完全支持 ACID 事务,并且因高可用性的承诺以及在横向伸缩环境中支持大型数据集而普遍被归类为“NoSQL 数据存储”。鉴于这些共同特征(与传统的 RDBMS 的特征形成鲜明对比),有人提议非关系(或者简称为 NonRel)是比 NoSQL 更为恰当的术语。

尽管定义冲突仍然存在,但很多人已经意识到将 NoSQL 加到其应用程序体系中的好处。其他人正保持密切关注并评估 NoSQL 是否适合他们。

NoSQL 作为一个类别的发展还导致了大量新数据存储的出现。其中某些新的 NoSQL 产品擅长持久保存像 JSON 这样的文档,某些按照列家族存储排序,其他的则持久保存分布式键值对。尽管更新的产品令人兴奋并且提供了很多好用的功能,但一些现有产品也在奋起直追履行新的承诺。

Oracle Berkeley DB 就是这样一个数据存储。在文本中,我将解释并说明为什么可以将 Berkeley DB 作为 NoSQL 解决方案包括在体系中以及具体实现方式。本文重点关注 Berkeley DB 围绕 NoSQL 的特性,因此不会详尽涵盖 Berkeley DB 的所有功能和特性。

Berkeley DB 要素

基本上,键值存储 Berkeley DB 有三种不同风格:

  • Berkeley DB — 用 C 编写的键值存储。(Berkeley DB 官方文档使用术语键-数据 代替键值。)这是“经典”风格。

  • Berkeley DB Java 版 (JE) — 用 Java 重新编写的键值存储。可以轻松包含在 Java 堆栈中。

  • Berkeley DB XML — 用 C 编写,此版本将键值存储进行包装,使其行为类似于一个已建立索引并且经过优化的 XML 存储系统。

(注意:尽管本文没有明确涉及 Berkeley DB JE 或 Berkeley DB XML,但是包括了一些使用 Java API 和基于 Java 的持久性框架来说明 Berkeley DB 功能的示例。)

Berkeley DB 的核心可能很简单,可以将它配置为提供并行非阻塞访问或支持事务,横向扩展为一个主从副本的高可用集群或者以多种其他方式横向扩展。

Berkeley DB 是一个纯存储引擎,不对键值对的隐式模式或结构做任何假设。因此,Berkeley DB 轻松允许在底层键值存储上实现更高级别的 API、查询和建模抽象。这有助于快速高效地存储应用程序特定数据,而不会产生将其转换为抽象数据格式的开销。这种简单却精致的设计所提供的灵活性能够在 Berkeley DB 中同时存储结构化和半结构化数据。

Berkeley DB 可作为内存中存储来运行,以保存少量数据,也可通过快速的内存中缓存配置为大型数据存储。在更高级别抽象(称作环境)的帮助下,可以在一个物理安装中配置多个数据库。一个环境可以有多个数据库。您需要打开一个环境,然后打开一个数据库,向其中写入数据或者从中读取数据。建议您在完成交互后关闭数据库和环境,从而以最佳方式使用资源。

数据库中的每一项都是一个键值对。键通常是唯一的,但是您可以有重复的项。值是通过键来访问的。可以更新检索值并将其保存回到数据库。通过游标对多个值进行访问和迭代。游标使您可以循环遍历值的集合以及同时操纵整个值集合。另外,还支持事务和并发访问。

键值对的键几乎总是充当建立索引的主键。值中的其他属性可充当次索引。在辅助数据库中单独维护次索引。因此,具有键值对的主要数据库有时候也被称作主数据库。

Berkeley DB 作为一个进程中数据存储运行,因此在使用 C、C++、C#、Java 或脚本语言 API 从相应程序中访问它时,您会以静态或动态方式链接到它。

简要介绍之后,下面将就 Berkeley DB 围绕 NoSQL 的特性进行介绍。

灵活的模式

NoSQL 存储的第一个优势是其对定义明确的数据库模式的宽松态度。我们来看看 Berkeley DB 如何实现此特性。

为了理解 Berkeley DB 的功能,建议您试用一下。因此,建议将 Berkeley DB 和 Berkeley DB JE 下载并安装到您的计算机上,这样您能亲自尝试一些示例并跟随本文中其余例证的操作。此处在线提供了下载链接和安装说明。(在本文中,我使用 --enable-java、--enable-sql 和 --prefix=/usr/local 对 Berkeley DB 进行了编译。)与存储、访问机制和 API 有关的基本概念在 Berkeley DB 和 Berkeley DB JE 之间没有太大区别,因此我后面涉及到的大部分内容同样适用于这两者。

除了数据项必须是键值对集合之外,Berkeley DB 本身对数据项的限制非常少。这就使得应用程序可以灵活地使用 Berkeley DB 管理各种格式的数据,包括 SQL、XML 和 Java 对象。您可以通过基础 API、SQL API、Java Collections API 以及 Java Direct Persistence Layer (DPL) 访问 Berkeley DB 中的数据。它允许几种不同存储配置:B 树、散列、队列和 Recno。(Berkeley DB 文档将不同存储机制称作“访问方法”。散列、队列和 Recno 访问方法仅在 Berkeley DB 中可用,在 Berkeley DB JE 或 Berkeley DB XML 中不可用。)

您可以根据具体用例来选择访问机制和存储配置。选择特定的访问方法和存储配置会影响模式。要了解您的选择所造成的影响,您需要先了解您所选的内容。我接下来要谈到访问方法和存储配置。

使用基础 API

基础 API 是低级别 API,使您可以存储、检索和更新数据(即键值对)。这种 API 在几种不同语言绑定之间是类似的。因此,C、C++ 和 Java 的基础 API 是完全相同的。另一方面,DPL 和 Java Collections API 仅作为抽象在 Java API 中提供。

基础 API 可放置、获取和删除键值对。键和值均为字节数组。在存储所有键和数据值之前,会将其序列化为字节数组。您可以使用 Java 的内置序列化程序或 Berkeley DB 的 BIND API 将各种数据类型序列化为字节数组。Java 的内置序列化程序通常执行速度较慢,因此用户必定更喜欢 BIND API。(jvm-serializers 项目对各种替代序列化程序进行基准测试,是用于在 JVM 的不同序列化机制之间分析相对性能的一个很好的参照点。)BIND API 可通过每个序列化类来避免冗余存储类信息,将该信息放在单独的数据库中。通过编写您自己的自定义字节组绑定来提高 BIND API 性能,您可以潜在地提高速度。

作为一个基本示例,您可以定义如下数据值:

import java.io.Serializable;
public class DataValue implements Serializable {
    private long prop1;
    private double prop2;

    DataValue() { 
      prop1 = 0;
      prop2 = 0.0;
    }

    public void setProp1(long data) {
      prop1 = data;
    }
    
    public long getProp1() {
      return prop1;
    }
    
    public void setProp2(double data) {
      prop2 = data;
    }
    
    public double getProp2() {
      return prop2;
    }
}


现在,您可以使用两个数据库来存储此数据值,一个数据库存储带有键的值,另一个数据库存储类信息。

使用四个不同步骤来存储数据:

  1. 首先,除了用于存储键值对的数据库之外的另一个数据库配置为存储类数据,如下所示:

    Database aClassDB = new Database("classDB", null, aDbConfig);
  2. 然后,将一个类目录实例化,如下所示:
    
    StoredClassCatalog storedClassCatalog = new StoredClassCatalog(aClassDb);
  3. 建立一个串行条目绑定,如下所示:
    
    EntryBinding binding = new SerialBinding(storedClassCatalog, DataValue.class);
  4. 最终,DataValue 实例如下所示:
    
    DataValue val = new DataValue();
    val.setProp1(123456789L);
    val.setProp2(1234.56789); 


    使用您刚创建的绑定映射到 Berkeley DB DatabaseEntry(充当键和值的包装器),如下所示:

    DatabaseEntry deKey = new DatabaseEntry(aKey.getBytes("UTF-8"));
    DatabaseEntry deVal = new DatabaseEntry();
    binding.objectToEntry(val, deVal);


现在,您可以将键值对放入 Berkeley DB 中。

基础 API 支持 put 和 get 方法的几种变体,以允许或不允许重复项和覆盖。(该示例以及本文都不是为了要教您有关如何使用基础 API 的详细语法或语义,因此我将不会涉及更多细节;请参阅这里的文档)。一个要点是,基础 API 允许就存储、检索和删除键值对进行低级操作和自定义序列化。

如果偏向于使用更高级的 API 与 Berkeley DB 进行交互,那么您应使用 DPL。

使用 DPL

直接持久层 (DPL) 提供了熟悉的 Java 持久性框架语义来操纵对象。您可以将 Berkeley DB 视作一个实体存储,对象在其中持久保存,并可对其中的对象进行检索以便更新和删除。DPL 使用批注将类标记为 @Entity。使用实体进行存储的相关联的类被注释为 @Persistent。特定属性或变量可以注释为 @PrimaryKey 和 @SecondaryKey。一个简单的实体可能如下所示:

@Entity
public class AnEntity {

    @PrimaryKey
    private int myPrimaryKey;

    @SecondaryKey(relate=ONE_TO_ONE)
    private String mySecondaryKey;
    ...
} 


DPL 将类定义用作定义明确的模式。通过基础 API,我们知道 Berkeley DB 不要求必须符合模式。但对于某些用例,正式的实体定义很有帮助并可为数据建模提供结构化方法。

存储配置

正如前面所提到的,可以通过四种不同类型的数据结构存储键值对:B 树、散列、队列和 Recno。我们来看看它们的效果如何。

  • B 树。需要对 B 树进行一些简要介绍,但如果您需要查看其定义,请阅读有关 B 树的 Wikipedia 页面 http://en.wikipedia.org/wiki/B-tree。这是一种平衡的树型数据结构,保证其元素经过排序并允许快速顺序访问、插入和删除。键和值可以为任意数据类型。在 Berkeley DB 中,B 树访问方法允许重复项。如果您需要用复杂数据类型作为键,这是一个不错的选择。如果数据访问模式导致访问相邻的记录,这也是一种很好的选择。B 树保存了大量元数据,可以高效地执行。大部分 Berkeley DB 应用程序使用 B 树存储配置。
  • 散列。与 B 树类似,散列也允许以复杂类型作为键。与 B 树相比,散列具有更加线性化的结构。Berkeley DB 散列结构允许重复项。

    尽管 B 树和散列均支持复杂键,但是当数据集远超过可用内存大小时,散列数据库的性能通常优于 B 树。这是因为 B 树比散列保存更多的元数据,更大的数据集意味着 B 树元数据可能无法存储在内存中缓存内。在这种极端情况下,B 树元数据以及实际数据记录本身通常必须取自文件,而这会导致每个操作有多个 I/0。散列访问方法旨在最大程度减少访问数据记录所需的 I/O 数量,因此在这些极端情况下,性能可能会优于 B 树。

  • 队列。队列是一组顺序存储的固定长度记录。键被限制为整数类型的逻辑记录编号。记录是按顺序追加,允许极快写入。如果您对 Apache Cassandra 通过向日志进行追加的快速写入印象深刻,那么请尝试采用队列访问方法的 Berkeley DB,您一定不会失望。这些方法还允许从队列的头有效地读取和更新。队列还支持行级锁定。这样即使是在并发处理的情况下,也能保证有效的事务完整性。
  • Recno。Recno 与队列类似,但是允许可变长度的记录。与队列类似,reco 键也被限制为整数。

不同配置使您可以在集合中存储任意类型的数据。与 NoSQL 类似,没有固定模式(除了您的模型实施的模式)。在极端情况中,您可以在集合中针对两个键分别存储不同的值类型。值类型可以是复杂类,就参数而言,可以表示 JSON 文档、复杂数据结构或结构化数据集。真正的唯一限制是,值应该序列化为字节数组。单个键或单个值最大可达 4GB。

次索引的出现允许根据值属性进行筛选。主数据库不会以表格格式来存储数据,因此不会为稀疏数据集存储非现有属性。如果键值对缺少用于创建索引的属性,次索引会跳过所有此类键值对。一般来说,这种存储方式既紧凑又高效。

对事务的支持

Berkeley DB 是一个非常灵活的数据库,可以打开和关闭许多特性。Berkeley DB 可以在不支持事务的情况下运行,也可以编译为支持 ACID 事务完整性。也许,Berkeley DB 的可塑性使其成为非常适合许多情况的数据存储。在典型的 NoSQL 数据存储中,对事务完整性的支持最差。在不期望 ACID 事务合规性的具有较高可用性的系统中,Berkeley DB 可以关闭事务,像典型的 NoSQL 产品那样工作。但是在其他系统中,它可能很灵活并且支持事务完整性。

尽管我并不打算涉及有关事务的细节,但值得注意的是,像传统 RDBMS 系统一样,支持事务的 Berkeley DB 允许定义事务边界。一旦提交,数据会持久保存到磁盘。为提高性能,您可以使用非持久性提交,这会将写操作提交到内存中日志文件,随后与底层文件系统进行同步。还支持隔离级别和锁定机制。

在数据库关闭之前,同步操作可保证持久文件副本在系统中具有最新的内存中信息。这种同步操作与 Berkeley DB 的事务恢复子系统的组合(假定您已启用了事务)可确保数据库始终返回到一致的事务状态,即使是在应用程序或系统发生故障时。

大型数据集

理论上,Berkeley DB 具有 256TB 的上限,但实际上,通常受运行 Berkeley DB 的计算机的大小限制。截至撰写本文时,Berkeley DB 未证实可在分布式文件系统的帮助下支持跨多台计算机的极大文件。(可借助 Hadoop 分布式文件系统 (HDFS) 等分布式文件系统的帮助管理超过单个节点大小的文件。)Berkeley DB 在本地文件系统上的性能优于在网络文件系统上的性能。更准确地说,Berkeley DB 依赖文件系统的 POSIX 兼容属性。例如,当 Berkeley DB 调用 fsync() 并且文件系统返回时,Berkeley DB 假定数据已写入到持久介质。出于性能原因,分布式文件系统通常不保证自始至终完成向持久介质的写入。

所支持的最大 B 树深度为 255。键和值的长度通常受可用内存的限制。

横向扩展

Berkeley DB 复制遵循主从模式。在此类模式中,有一个主节点和多个从属节点(或副本)。但是,主节点的选择不是静态的,并且不建议手动选择。复制集群中的所有参与节点都要经历一个选举过程以选出主节点。具有最新日志记录的参与节点将成为获胜者。如果具有绑定,那么优先级用于选择主节点。选举过程基于行业标准的符合 Paxos 的算法

复制具有很多好处,包括:

  • 提高读性能 — 可从多个副本节点中读取数据极大提高了读性能。

  • 提高可靠性 — 有了副本实例,就可以在发生节点故障和数据损坏时提供更好的故障转移选择。

  • 提高持久性 — 您可以放宽对主节点的持久性保证以避免过多地对磁盘进行写入操作,这通常需要昂贵的 I/O。在集群环境中,通过将写入提交到多个节点(即使未写入到磁盘)这一事实增强了持久性。

  • 提高可用性 — 由于有多个节点并且对磁盘进行异步写入,即使在主节点负载过高的情况下,副本节点仍可继续提供服务。

总结

毫无疑问,Berkeley DB 作为一个强健、可伸缩的 NoSQL 键值存储非常合格;Amazon 的 DynamoProject VoldemortMemcacheDBGenieDB 使用 Berkeley DB 作为底层存储就是支持这一观点的进一步证据。围绕 Berkeley DB 性能一直存在一些恐惧,尤其是下面这两个在线发布的比较基准测试:

但是,很多运行中的系统证明了 Berkeley DB 的强大。其中许多系统经过了仔细的调整和应用程序编码改进,已经获得了出色的可伸缩性、吞吐量和可靠性结果。效法这些系统,Berkeley DB 无疑可用作可伸缩的 NoSQL 解决方案。


Shashank TiwariTreasury of Ideas(一家技术驱动的创新和价值优化公司)的创始人兼 CEO。作为一名经验丰富的软件开发人员和架构师,他精通大量技术。他是国际认可的演讲者、作者和导师。作为数种 JCP (Java Community Process) 规范的专家组成员,他一直积极参与规划 Java 的未来。他还代表了 NoSQL 和云计算领域的心声,是 RIA 社区公认的专家。