将 Oracle Berkeley DB 用作 NoSQL 数据存储
“NoSQL”是在开发人员、架构师甚至技术经理中新流行的一个词汇。尽管这个术语最近很流行,但令人惊讶的是,它并没有一个普遍认可的定义。
通常来说,任何非RDBMS且遵循无模式结构的数据库一般都不能完全支持ACID事务,并且因高可用性的承诺以及在横向伸缩环境中支持大型数据集而普遍被归类为“NoSQL数据存储”。鉴于这些共同特征(与传统的RDBMS的特征形成鲜明对比),有人提议非关系(或者简称为NonRel)是比NoSQL更为恰当的术语。
尽管定义冲突仍然存在,但很多人已经意识到将NoSQL加到其应用程序体系中的好处。其他人正保持密切关注并评估NoSQL是否适合他们。
NoSQL作为一个类别的发展还导致了大量新数据存储的出现。其中某些新的NoSQL产品擅长持久保存像JSON这样的文档,某些按照列家族存储排序,其他的则持久保存分布式键值对。尽管更新的产品令人兴奋并且提供了很多好用的功能,但一些现有产品也在奋起直追履行新的承诺。
OracleBerkeleyDB就是这样一个数据存储。在文本中,我将解释并说明为什么可以将BerkeleyDB作为NoSQL解决方案包括在体系中以及具体实现方式。本文重点关注BerkeleyDB围绕NoSQL的特性,因此不会详尽涵盖BerkeleyDB的所有功能和特性。
BerkeleyDB要素
基本上,键值存储BerkeleyDB有三种不同风格:
BerkeleyDB—用C编写的键值存储。(BerkeleyDB官方文档使用术语键-数据代替键值。)这是“经典”风格。
BerkeleyDBJava版(JE)—用Java重新编写的键值存储。可以轻松包含在Java堆栈中。
BerkeleyDBXML—用C编写,此版本将键值存储进行包装,使其行为类似于一个已建立索引并且经过优化的XML存储系统。
(注意:尽管本文没有明确涉及BerkeleyDBJE或BerkeleyDBXML,但是包括了一些使用JavaAPI和基于Java的持久性框架来说明BerkeleyDB功能的示例。)
BerkeleyDB的核心可能很简单,可以将它配置为提供并行非阻塞访问或支持事务,横向扩展为一个主从副本的高可用集群或者以多种其他方式横向扩展。
BerkeleyDB是一个纯存储引擎,不对键值对的隐式模式或结构做任何假设。因此,BerkeleyDB轻松允许在底层键值存储上实现更高级别的API、查询和建模抽象。这有助于快速高效地存储应用程序特定数据,而不会产生将其转换为抽象数据格式的开销。这种简单却精致的设计所提供的灵活性能够在BerkeleyDB中同时存储结构化和半结构化数据。
BerkeleyDB可作为内存中存储来运行,以保存少量数据,也可通过快速的内存中缓存配置为大型数据存储。在更高级别抽象(称作环境)的帮助下,可以在一个物理安装中配置多个数据库。一个环境可以有多个数据库。您需要打开一个环境,然后打开一个数据库,向其中写入数据或者从中读取数据。建议您在完成交互后关闭数据库和环境,从而以最佳方式使用资源。
数据库中的每一项都是一个键值对。键通常是唯一的,但是您可以有重复的项。值是通过键来访问的。可以更新检索值并将其保存回到数据库。通过游标对多个值进行访问和迭代。游标使您可以循环遍历值的集合以及同时操纵整个值集合。另外,还支持事务和并发访问。
键值对的键几乎总是充当建立索引的主键。值中的其他属性可充当次索引。在辅助数据库中单独维护次索引。因此,具有键值对的主要数据库有时候也被称作主数据库。
BerkeleyDB作为一个进程中数据存储运行,因此在使用C、C++、C#、Java或脚本语言API从相应程序中访问它时,您会以静态或动态方式链接到它。
简要介绍之后,下面将就BerkeleyDB围绕NoSQL的特性进行介绍。
灵活的模式
NoSQL存储的第一个优势是其对定义明确的数据库模式的宽松态度。我们来看看BerkeleyDB如何实现此特性。
为了理解BerkeleyDB的功能,建议您试用一下。因此,建议将BerkeleyDB和BerkeleyDBJE下载并安装到您的计算机上,这样您能亲自尝试一些示例并跟随本文中其余例证的操作。此处在线提供了下载链接和安装说明。(在本文中,我使用--enable-java、--enable-sql和--prefix=/usr/local对BerkeleyDB进行了编译。)与存储、访问机制和API有关的基本概念在BerkeleyDB和BerkeleyDBJE之间没有太大区别,因此我后面涉及到的大部分内容同样适用于这两者。
除了数据项必须是键值对集合之外,BerkeleyDB本身对数据项的限制非常少。这就使得应用程序可以灵活地使用BerkeleyDB管理各种格式的数据,包括SQL、XML和Java对象。您可以通过基础API、SQLAPI、JavaCollectionsAPI以及JavaDirectPersistenceLayer(DPL)访问BerkeleyDB中的数据。它允许几种不同存储配置:B树、散列、队列和Recno。(BerkeleyDB文档将不同存储机制称作“访问方法”。散列、队列和Recno访问方法仅在BerkeleyDB中可用,在BerkeleyDBJE或BerkeleyDBXML中不可用。)
您可以根据具体用例来选择访问机制和存储配置。选择特定的访问方法和存储配置会影响模式。要了解您的选择所造成的影响,您需要先了解您所选的内容。我接下来要谈到访问方法和存储配置。
使用基础API
基础API是低级别API,使您可以存储、检索和更新数据(即键值对)。这种API在几种不同语言绑定之间是类似的。因此,C、C++和Java的基础API是完全相同的。另一方面,DPL和JavaCollectionsAPI仅作为抽象在JavaAPI中提供。
基础API可放置、获取和删除键值对。键和值均为字节数组。在存储所有键和数据值之前,会将其序列化为字节数组。您可以使用Java的内置序列化程序或BerkeleyDB的BINDAPI将各种数据类型序列化为字节数组。Java的内置序列化程序通常执行速度较慢,因此用户必定更喜欢BINDAPI。(jvm-serializers项目对各种替代序列化程序进行基准测试,是用于在JVM的不同序列化机制之间分析相对性能的一个很好的参照点。)BINDAPI可通过每个序列化类来避免冗余存储类信息,将该信息放在单独的数据库中。通过编写您自己的自定义字节组绑定来提高BINDAPI性能,您可以潜在地提高速度。
作为一个基本示例,您可以定义如下数据值:
importjava.io.Serializable;
publicclassDataValueimplementsSerializable{
privatelongprop1;
privatedoubleprop2;
DataValue(){
prop1=0;
prop2=0.0;
}
publicvoidsetProp1(longdata){
prop1=data;
}
publiclonggetProp1(){
returnprop1;
}
publicvoidsetProp2(doubledata){
prop2=data;
}
publicdoublegetProp2(){
returnprop2;
}
}
现在,您可以使用两个数据库来存储此数据值,一个数据库存储带有键的值,另一个数据库存储类信息。
使用四个不同步骤来存储数据:
1.首先,除了用于存储键值对的数据库之外的另一个数据库配置为存储类数据,如下所示:
DatabaseaClassDB=newDatabase("classDB",null,aDbConfig);2.然后,将一个类目录实例化,如下所示:
StoredClassCatalogstoredClassCatalog=newStoredClassCatalog(aClassDb);3.建立一个串行条目绑定,如下所示:
EntryBindingbinding=newSerialBinding(storedClassCatalog,DataValue.class);4.最终,DataValue实例如下所示:
DataValueval=newDataValue();
val.setProp1(123456789L);
val.setProp2(1234.56789);
使用您刚创建的绑定映射到BerkeleyDBDatabaseEntry(充当键和值的包装器),如下所示:
DatabaseEntrydeKey=newDatabaseEntry(aKey.getBytes("UTF-8"));
DatabaseEntrydeVal=newDatabaseEntry();
binding.objectToEntry(val,deVal);
现在,您可以将键值对放入BerkeleyDB中。
基础API支持put和get方法的几种变体,以允许或不允许重复项和覆盖。(该示例以及本文都不是为了要教您有关如何使用基础API的详细语法或语义,因此我将不会涉及更多细节;请参阅这里的文档)。一个要点是,基础API允许就存储、检索和删除键值对进行低级操作和自定义序列化。
如果偏向于使用更高级的API与BerkeleyDB进行交互,那么您应使用DPL。
使用DPL
直接持久层(DPL)提供了熟悉的Java持久性框架语义来操纵对象。您可以将BerkeleyDB视作一个实体存储,对象在其中持久保存,并可对其中的对象进行检索以便更新和删除。DPL使用批注将类标记为@Entity。使用实体进行存储的相关联的类被注释为@Persistent。特定属性或变量可以注释为@PrimaryKey和@SecondaryKey。一个简单的实体可能如下所示:
@Entity
publicclassAnEntity{
@PrimaryKey
privateintmyPrimaryKey;
@SecondaryKey(relate=ONE_TO_ONE)
privateStringmySecondaryKey;
...
}
DPL将类定义用作定义明确的模式。通过基础API,我们知道BerkeleyDB不要求必须符合模式。但对于某些用例,正式的实体定义很有帮助并可为数据建模提供结构化方法。
存储配置
正如前面所提到的,可以通过四种不同类型的数据结构存储键值对:B树、散列、队列和Recno。我们来看看它们的效果如何。
B树。需要对B树进行一些简要介绍,但如果您需要查看其定义,请阅读有关B树的Wikipedia页面http://en.wikipedia.org/wiki/B-tree。这是一种平衡的树型数据结构,保证其元素经过排序并允许快速顺序访问、插入和删除。键和值可以为任意数据类型。在BerkeleyDB中,B树访问方法允许重复项。如果您需要用复杂数据类型作为键,这是一个不错的选择。如果数据访问模式导致访问相邻的记录,这也是一种很好的选择。B树保存了大量元数据,可以高效地执行。大部分BerkeleyDB应用程序使用B树存储配置。
散列。与B树类似,散列也允许以复杂类型作为键。与B树相比,散列具有更加线性化的结构。BerkeleyDB散列结构允许重复项。
尽管B树和散列均支持复杂键,但是当数据集远超过可用内存大小时,散列数据库的性能通常优于B树。这是因为B树比散列保存更多的元数据,更大的数据集意味着B树元数据可能无法存储在内存中缓存内。在这种极端情况下,B树元数据以及实际数据记录本身通常必须取自文件,而这会导致每个操作有多个I/0。散列访问方法旨在最大程度减少访问数据记录所需的I/O数量,因此在这些极端情况下,性能可能会优于B树。
队列。队列是一组顺序存储的固定长度记录。键被限制为整数类型的逻辑记录编号。记录是按顺序追加,允许极快写入。如果您对ApacheCassandra通过向日志进行追加的快速写入印象深刻,那么请尝试采用队列访问方法的BerkeleyDB,您一定不会失望。这些方法还允许从队列的头有效地读取和更新。队列还支持行级锁定。这样即使是在并发处理的情况下,也能保证有效的事务完整性。
Recno。Recno与队列类似,但是允许可变长度的记录。与队列类似,reco键也被限制为整数。
不同配置使您可以在集合中存储任意类型的数据。与NoSQL类似,没有固定模式(除了您的模型实施的模式)。在极端情况中,您可以在集合中针对两个键分别存储不同的值类型。值类型可以是复杂类,就参数而言,可以表示JSON文档、复杂数据结构或结构化数据集。真正的唯一限制是,值应该序列化为字节数组。单个键或单个值最大可达4GB。
次索引的出现允许根据值属性进行筛选。主数据库不会以表格格式来存储数据,因此不会为稀疏数据集存储非现有属性。如果键值对缺少用于创建索引的属性,次索引会跳过所有此类键值对。一般来说,这种存储方式既紧凑又高效。
对事务的支持
BerkeleyDB是一个非常灵活的数据库,可以打开和关闭许多特性。BerkeleyDB可以在不支持事务的情况下运行,也可以编译为支持ACID事务完整性。也许,BerkeleyDB的可塑性使其成为非常适合许多情况的数据存储。在典型的NoSQL数据存储中,对事务完整性的支持最差。在不期望ACID事务合规性的具有较高可用性的系统中,BerkeleyDB可以关闭事务,像典型的NoSQL产品那样工作。但是在其他系统中,它可能很灵活并且支持事务完整性。
尽管我并不打算涉及有关事务的细节,但值得注意的是,像传统RDBMS系统一样,支持事务的BerkeleyDB允许定义事务边界。一旦提交,数据会持久保存到磁盘。为提高性能,您可以使用非持久性提交,这会将写操作提交到内存中日志文件,随后与底层文件系统进行同步。还支持隔离级别和锁定机制。
在数据库关闭之前,同步操作可保证持久文件副本在系统中具有最新的内存中信息。这种同步操作与BerkeleyDB的事务恢复子系统的组合(假定您已启用了事务)可确保数据库始终返回到一致的事务状态,即使是在应用程序或系统发生故障时。
大型数据集
理论上,BerkeleyDB具有256TB的上限,但实际上,通常受运行BerkeleyDB的计算机的大小限制。截至撰写本文时,BerkeleyDB未证实可在分布式文件系统的帮助下支持跨多台计算机的极大文件。(可借助Hadoop分布式文件系统(HDFS)等分布式文件系统的帮助管理超过单个节点大小的文件。)BerkeleyDB在本地文件系统上的性能优于在网络文件系统上的性能。更准确地说,BerkeleyDB依赖文件系统的POSIX兼容属性。例如,当BerkeleyDB调用fsync()并且文件系统返回时,BerkeleyDB假定数据已写入到持久介质。出于性能原因,分布式文件系统通常不保证自始至终完成向持久介质的写入。
所支持的最大B树深度为255。键和值的长度通常受可用内存的限制。
横向扩展
BerkeleyDB复制遵循主从模式。在此类模式中,有一个主节点和多个从属节点(或副本)。但是,主节点的选择不是静态的,并且不建议手动选择。复制集群中的所有参与节点都要经历一个选举过程以选出主节点。具有最新日志记录的参与节点将成为获胜者。如果具有绑定,那么优先级用于选择主节点。选举过程基于行业标准的符合Paxos的算法。
复制具有很多好处,包括:
提高读性能—可从多个副本节点中读取数据极大提高了读性能。
提高可靠性—有了副本实例,就可以在发生节点故障和数据损坏时提供更好的故障转移选择。
提高持久性—您可以放宽对主节点的持久性保证以避免过多地对磁盘进行写入操作,这通常需要昂贵的I/O。在集群环境中,通过将写入提交到多个节点(即使未写入到磁盘)这一事实增强了持久性。
提高可用性—由于有多个节点并且对磁盘进行异步写入,即使在主节点负载过高的情况下,副本节点仍可继续提供服务。
总结
毫无疑问,BerkeleyDB作为一个强健、可伸缩的NoSQL键值存储非常合格;Amazon的Dynamo、ProjectVoldemort、MemcacheDB和GenieDB使用BerkeleyDB作为底层存储就是支持这一观点的进一步证据。围绕BerkeleyDB性能一直存在一些恐惧,尤其是下面这两个在线发布的比较基准测试:
http://www.dmo.ca/blog/benchmarking-hash-databases-on-large-data/
http://stackoverflow.com/questions/601348/berkeleydb-vs-tokyo-cabinet
但是,很多运行中的系统证明了BerkeleyDB的强大。其中许多系统经过了仔细的调整和应用程序编码改进,已经获得了出色的可伸缩性、吞吐量和可靠性结果。效法这些系统,BerkeleyDB无疑可用作可伸缩的NoSQL解决方案。
--------------------------------------------------------------------------------
ShashankTiwari是TreasuryofIdeas(一家技术驱动的创新和价值优化公司)的创始人兼CEO。作为一名经验丰富的软件开发人员和架构师,他精通大量技术。他是国际认可的演讲者、作者和导师。作为数种JCP(JavaCommunityProcess)规范的专家组成员,他一直积极参与规划Java的未来。他还代表了NoSQL和云计算领域的心声,是RIA社区公认的专家。