mapDB文档
介绍
MapDB是一个开源的嵌入式Java数据引擎和集合框架。它提供了Maps,Sets,Lists,Queues,Bitmaps的范围查询、数据过期机制、数据压缩、堆外存储和流式操作。MapDB可能是一个最快的Java数据库,它能够和 java.util
集合相媲美。它同时也拥有一些高级特性,比如ACID的事务、快照、增量备份等等。
这个文档还在编写过程中,它将与 MapDbB 3.0 共同完成。我们希望你能觉得它是有帮助的。如果你想对MapDB做贡献,我们将非常高兴接受你的pull requests。
这个文档中的代码示例都在 github 仓库中。
快速开始
快速简介
MapDB是灵活的,有许多可配置的选项。但是在大多数情况下,它和配置仅仅是几行代码而已。
TODO 许多资源:备忘记录,例子等
获取它
MapDB的包发布在Maven的中心仓库中。下面是MapDB依赖的代码片段。
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>VERSION</version>
</dependency>
VERSION
是指Maven Central中的最新的版本号。版本最新的快照版本在这。
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>VERSION</version>
</dependency>
</dependencies>
你也可以直接从Maven Centra下载MapDB的jar包。如果那样的话,你要记住MapDB也是依赖Eclise Collections, Guava, Kotlin库和其他资源库的。这里是每个版本的所有的依赖列表。
Hello World
下面是一个简单的例子。它在内存中打开一个HashMap,它使用堆外内存,它不受垃圾回归所限制。
//import org.mapdb.*
DB db = DBMaker.memoryDB().make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");
HashMap(和其他集合)也能够存储在文件里。如果使用这样种方式,当JVM重启时,数据也可以保存下来。但是必须要调用 DB.close()
方法防止出现数据损坏的情况。其他可以选择的是启用写日志先进行的事务选项。
DB db = DBMaker.fileDB("file.db").make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");
db.close();
TODO Hello Word 例子没有覆盖 commit。
默认情况下,MapDB使用通用的序列化方式,它可以序列化任何数据类型。更快的方式是使用特定的序列化器。同样地,我们也能够在64位操作系统上启用更快的内存映射文件。
DB db = DBMaker
.fileDB("file.db")
.fileMmapEnable()
.make();
ConcurrentMap<String,Long> map = db
.hashMap("map", Serializer.STRING, Serializer.LONG)
.createOrOpen();
map.put("something", 111L);
db.close();
示例项目
TODO
小技巧
- 内存映射文件能够更快,为了更高的性能,在64位操作系统中它应该被启用。
- MapDB拥有更快的批量导入的集合,它比
Map.put
要理他快。 - 事务有一定的性能开销,但是没有正确的关闭,也没有事务就可能出现数据损坏的情况。
- 数据储存在MapDB中应该是不可更改的。MapDB序列化是在后台执行的。
- MapDB有时需要进行数据压缩。执行
DB.compact()
或者看后台压缩选项。
数据库
DB 和 DBMaker
MapDB像乐高一样是可插拔的。DB
和 DBMaker
这两个类像胶水一样连接着不同的部分。
DBMaker 处理数据库配置、创建和打开。MapDB拥有多个模式和多种可选配置。绝大多数的都是使用这个类进行配置的。
一个DB实例代表着一个打开的数据库(或者是一个事务会话)。它可以用来创建或者打开存储集合。它也能够使用一些方法来管理数据库的生命周期,比如 commit()
, rollback()
和 close()
。
可以使用任何一个名称为 *DB
的静态方法来打开(或者创建)一个存储,比如 DBMaker.fileDB()
。MapDB拥有很多格式和模式。每个 xxxDB()
都代表不同的模式,比如 memoryDB()
为打开一个使用 byte[]
数组的内存数据库,appendFileDB()
为打开一个使用只在日志文件末尾添加日志的数据库等。
一个 xxxDB()
方法后面会有一个或者多个配置选项,最后的一个 make()
方法表示应用所有选项,并且打开已选择的存储,返回一个数据库类。下面的例子就是打开一个能够加密的文件存储。
DB db = DBMaker
.fileDB("/some/file")
//TODO encryption API
//.encryptionEnable("password")
.make();
打开或者创建集合
一但你拥有了数据库类,你就可以打开一个集合或者其他记录。数据库使用建造者模式进行配置。它使用集合类型和名称开头,紧跟着是可以应用的配置,最后是操作指示符。
下面的例子就是打开(或者创建)名为 example 的 TreeSet数据库。
NavigableSet treeSet = db.treeSet("example").createOrOpen();
你也可以应用额外的应用配置:
NavigableSet<String> treeSet = db
.treeSet("treeSet")
.maxNodeSize(112)
.serializer(Serializer.STRING)
.createOrOpen();
这个建造者可以使用三种模式完成:
create()
将创建一个新集合,如果该集合已经存在会抛出异常。open()
会打开一个已经存在的集合,如果集合不存在会抛出异常。createOrOpen()
会打开一个已经存在的集合,如果命令不存在会创建一个。
数据库并不仅限制于集合,它还可以创建其他类型的记录,比如原子记录。
Atomic.Var<Person> var = db.atomicVar("mainPerson",Person.SERIALIZER).createOrOpen();
事务
数据库拥有三个方法来管理事务的生命周期,分别是 commit()
,rollback()
和 close()
。
一个数据库类代表一个事务。上面的例子中的每个存储都是使用单个全局事务,它对一些场景也足够使用了。
ConcurrentNavigableMap<Integer,String> map = db
.treeMap("collectionName", Serializer.INTEGER, Serializer.STRING)
.createOrOpen();
map.put(1,"one");
map.put(2,"two");
// 在提交之前 map.keySet() 是 [1,2]
db.commit(); // 持久化数据到磁盘
map.put(3,"three");
//map.keySet() is now [1,2,3]
db.rollback(); // 回滚最近的操作
//map.keySet() is now [1,2]
db.close();
HTreeMap
HTreeMap 为MapDB提供了 HashMap
和 HashSet
。它支持数据过期,可以用来当缓存使用。它是线程安全的,支持并行更新扩展操作。
它是线程是安全的,使用分段技术支持并行写,每一段都是一个读写锁。这和JDK7中的 ConcurrentHashMap
的工作原理是相似的。分段的数量(也叫并发因子)是可配置的。
HTreeMap是一棵分段树哈希树。不像其他HashMap,它并不会使用固定大小的哈希表,哈希表增长时它也不会重新调哈希表。HTreeMap使用的是可以自动扩展的索引树,所以它不需要调整。它也使用更少的空间,因为空的哈希槽不会浪费任何空间。另一方面,这这个树结构要求更多的搜索,并且访问比较慢。TODO 随着规模的变化,它的性能能下降到什么程度呢?
HTreeMap基于四个原则支持节点过期的分别是:最大的map节点数量,最大的储存大小,最后一次修改时间和最后一次访问时间。过期节点会被自动移除。这个特性使用先进先出的队列,并且每个分段都拥有自己独立的过期队列。
序列化器
HTreeMap拥有一些参数。最重要的参数就是名称,它唯一标记着Map在数据库类中和处理内部数据的序列化器:
HTreeMap<String, Long> map = db.hashMap("name_of_map")
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.LONG)
.create();
// 更简洁的形式
HTreeMap<String, Long> map2 = db
.hashMap("some_other_map", Serializer.STRING, Serializer.LONG)
.create();
也可以直接跳过定义序列化器,但是MapDB会使用比较慢的通用的序列化方式,这是不推荐的:
HTreeMap map = db
.hashMap("name_of_map")
.create();
推荐使用HTreeMap处理大键或者大值。在这些情况下,你可能想使用数据压缩。它可以启用数据压缩,但是会损耗一些性能。相反,最好的只对使用了特定的序列化器的键或值进行压缩。这是通过使用序列化包装器来完成的:
HTreeMap<Long, String> map = db.hashMap("map")
.valueSerializer(
new SerializerCompressionWrapper(Serializer.STRING))
.create();
Hash Code
大多数哈希表都是使用由 Object.hashCode()
生成的32位哈希码,然后使用 Object.equals()
来检查是否相等。然而很多类(byte[]
,int[]
)都没有正确地实现这个方法。
MapDB 使用键序列化器来生成哈希值并进行比较。例如,如果Serializer.BYTE_ARRAY 能够被当键序列化器,那么byte[]
能够直接作为HTreeMap的键。
HTreeMap<byte[], Long> map = db.hashMap("map")
.keySerializer(Serializer.BYTE_ARRAY)
.valueSerializer(Serializer.LONG)
.create();
另一个问题就是在一些类里的弱哈希会造成哈希冲突,从而降低性能。String.hashCode()
是弱哈希,但是它是规范的一部分,无法改变。在JDK中HashMap
的实现有很多昂贵的内存和性能的开销为代价的解决方案,但是HTreeMap
并没有这些解决方案,所以虚哈希会极大的降低它的性能。
同时,HTreeMap
正在从根源上修改这个问题,Serializer.STRING
使用了强哈希,能够减少哈希冲突。String.hashCode()
仍然能够使用,但是是使用不同的序列化器。
// 使用强哈希的
HTreeMap<String, Long> map = db.hashMap("map")
// by default it uses strong XXHash
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.LONG)
.create();
// 使用弱哈希 `String.hashCode()`
HTreeMap<String, Long> map2 = db.hashMap("map2")
// use weak String.hashCode()
.keySerializer(Serializer.STRING_ORIGHASH)
.valueSerializer(Serializer.LONG)
.create();
HashMap容易受到哈希冲突攻击。HTreeMap
提供了散列种子的保护机制。当集合被创建时,它会随机生成,同时会持久化到集合的定义中。用户也可以自己定义散列种子。
HTreeMap<String, Long> map = db
.hashMap("map", Serializer.STRING, Serializer.LONG)
.hashSeed(111) //force Hash Seed value
.create();
TODO 64位哈希
TODO 使用DataIO.hashInt(),自定义哈希值生成,并按bit扩展。
Layout
HashMap
拥有一些参数,比如初始容量、负载因子等。MapDB也有几套不同的参数,用来控制它的访问时间和最大大小。这些都被分组在特定Map设计下。
并发是使用分段来实现的,每段都有独立的读写锁。每个并发段都是独立的,它拥有自己的节点数量计数器,遍历迭代器和过期队列。分段的数量是可配置的。分段数量太少,当并发更新时会导致出现拥塞问题,分类太多会增加内存开销。
HTreeMap
使用索引树,而不是使用增长的Object[]
的哈希表。索引树是一种稀疏的数组数据结构,它是一个层次数组。由于它是稀疏的,所以没有使用的节点不会占用空间。它不进行重新哈希操作(复制所有节点到一个更大的数组中)也不会超过其初始设计的容量。
HTreeMap
的设计是通过 layout
方法控制的。它拥有三个参数:
- 并发数,分段的数量。默认为8,它总是会被取整到2的幂。
- 索引树直接节点的最大的节点数量。默认为16,它总是会被取整到2的幂,最大值为128个节点。
- 索引树的高度。默认为4。
最大的哈希表的数量计算公式为 分段数 * 节点数 ^ 索引高度
。默认的最大数量为 8*16^4=
50万个。
TODO 太低?
如果哈希表的大小设置的太低,一但它填满了,哈希冲突就会出现,性能就会降低。哈希表填满了,HTreeMap
也会接受新的节点,但是性能将会降低。
32位哈希设置的上限个为:40亿个。有一个计划会支持64位哈希。
其他参数
另一个参数是大小计数器。默认情况下,HTreeMap不会追踪它的大小,map.size()
会进行一个线性扫描所有的节点去计数。你也可以启用大小计数器,这样的话, map.size()
就会立即返回,但是会在插入节点时有开销。
HTreeMap<String, Long> map = db
.hashMap("map", Serializer.STRING, Serializer.LONG)
.counterEnable()
.create();
最后有一些语法糖。有一个 value loader,它是一个方法,当键不存在时,能够加载一个值。一个最新的键值会插入到数据库中。map.get(key)
这个方法不会返回 null
。这个主要使用于各种生成器和缓存。
HTreeMap<String,Long> map = db
.hashMap("map", Serializer.STRING, Serializer.LONG)
.valueLoader(s -> 1L)
.create();
// 尽管键不存在,但是返回1
Long one = map.get("Non Existent");
// 值生成的输出,会被插入到数据库中
map.size(); // => 1
分片存储能够获得更好的并发
HTreeMap
会被切分成几个分段。每个分段都是独立的,并且不会和其他分段共享任何状态。但是它们仍然会共享底层存储,这会影响并发负载的性能。通过对每个分段分配独立的储存,可以真正实现分段独立。
那个就是 Sharded HTreeMap,可以直接通过DBmaker创建:
HTreeMap<String, byte[]> map = DBMaker
// 参数是储存的数量(并发因子)
.memoryShardedHashMap(8)
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.BYTE_ARRAY)
.create();
// 数据库不存在,因此直接关闭
map.close();
当通过数据库类创建时,Sharded HTreeMap同HTreeMap拥有相似的配置。但是没有数据库类与这个HTreeMap相关联。因此为了关闭Sharded HTreeMap,就需要直接调用 HTreeMap.close()
。
数据过期
如果达到某些条件,HTreeMap
提供可供选择的节点过期机制。
- 节点存在时间超过过期时间。这个过期的开始时间可能是创建时间、最后一次修改时间和最后一次访问时间。
- map中的节点数量超过最大数量。
- map使用的内存或者是磁盘超过限制大小。
下面将设置过期从创建时间、最后一次更新时间和最后一次访问时间开始:
// 删除最后一次更新时间超过10分钟的节点
// 或者 1分钟内没有访问的节点
HTreeMap cache = db
.hashMap("cache")
.expireAfterUpdate(10, TimeUnit.MINUTES)
.expireAfterCreate(10, TimeUnit.MINUTES)
.expireAfterGet(1, TimeUnit.MINUTES)
.create();
下面将创建有内存限制为16GB的 HTreeMap
:
// 堆外内存最大为 16GB
Map cache = db
.hashMap("map")
.expireStoreSize(16 * 1024*1024*1024)
.expireAfterGet()
.create();
它也可以限制最大的节点数量:
HTreeMap cache = db
.hashMap("cache")
.expireMaxSize(128)
.expireAfterGet()
.create();
HTreeMap 对每个分段使用后进先出的过期队列,遍历队列并删除最早的节点。并不是所有的节点都会放到过期队列中。比如,在下面这个例子中,新生成的节点永远不会过期,只有经过更新后的节点(值发生了变化)才会被放到过期队列中。
HTreeMap cache = db
.hashMap("cache")
.expireAfterUpdate(1000)
.create();
基于时间的放逐节点总是会放到过期队列中。但是其他过期标准也是需要提示如何将节点放到过期队列中。在下面的例子中,没有节点会放到队列中,节点永不过期。
HTreeMap cache = db
.hashMap("cache")
.expireMaxSize(1000)
.create();
有三种触发器会将节点放入过期队列:expireAfterCreate()
,expireAfterUpdate()
,expireAfterGet()
。需要的注意的是这些方法都没有生存时间。
节点过期是在其他方法中处理的。如果你使用 map.put()
或者 map.get()
它可能会逐出一起过期节点。但是这个逐出节点是有一些开销的,它会减慢用户的操作。对于HTreeMap来说,可以使用一个线程池在后台处理。下面的例子是两个后台线程,并且每10秒钟触发一次。
DB db = DBMaker.memoryDB().make();
ScheduledExecutorService executor =
Executors.newScheduledThreadPool(2);
HTreeMap cache = db
.hashMap("cache")
.expireMaxSize(1000)
.expireAfterGet()
.expireExecutor(executor)
.expireExecutorPeriod(10000)
.create();
// 一但我们操作未完成,后台线程是需要关闭的
db.close();
过期机制可以结合多分片HTreeMap使用,能够提供更好的并发性。这样的话,每个分片都拥有独立存储,能够提供并发更新的伸缩性。
HTreeMap cache = DBMaker
.memoryShardedHashMap(16)
.expireAfterUpdate()
.expireStoreSize(128*1024*1024)
.create();
分片HTreeMap也应该结合后台线程池做数据放逐。同样地,随着时间推移,存储会变得碎片化,最后空间不能够回收利用。如果有太多空闲时间,可以使用计划定时压缩。压缩能够回收空闲时间。由于每个存储(分段)都是单独压缩的,所以不会影响正在运行的线程。
HTreeMap cache = DBMaker
.memoryShardedHashMap(16)
.expireAfterUpdate()
.expireStoreSize(128*1024*1024)
// 3个线程用来处理节点过期
.expireExecutor(
Executors.newScheduledThreadPool(3))
// 当空闲时间超过 40% 时压缩空间
.expireCompactThreshold(0.4)
.create();
过期溢出
HTreeMap运行修改监听器。它可以提醒监听者HTreeMap发生的插入、更新和删除操作。它能够把两个集合连接起来。通常情况下,访问速度快的内存空间是限的,而访问速度慢的磁盘的空间是无限的。节点经过内存过期后,会自动被修改监听器移到硬盘上。如果使用 map.get()
没有查找到数据,值加载器会重新将数据加载到内存中。
可以使用以下代码创建数据溢出到磁盘:
DB dbDisk = DBMaker
.fileDB(file)
.make();
DB dbMemory = DBMaker
.memoryDB()
.make();
// 从缓存中填充大量过期数据
HTreeMap onDisk = dbDisk
.hashMap("onDisk")
.create();
// 内存访问快,但空间有限制
HTreeMap inMemory = dbMemory
.hashMap("inMemory")
.expireAfterGet(1, TimeUnit.SECONDS)
// 注册溢出到磁盘
.expireOverflow(onDisk)
// 启用后台过期是一个好做法
.expireExecutor(Executors.newScheduledThreadPool(2))
.create();
一但两者绑定成功,每个节点都会从内存被添加到磁盘存储。这个只适用于过期节点,使用 map.remove()
也可以将任何节点从磁盘中删除。
// 手动插入,会使两个存储都有
inMemory.put("key", "map");
// 从内存存储中删除
inMemory.remove("key");
onDisk.get("key"); // 磁盘中也找不到了
如果使用在内存存储中获取一个值找不到的话,值加载器会尝试从磁盘存储中查找,如果查找到了,就会将它加载到内存存储中。
onDisk.put(1,"one"); // 磁盘存储有数据,而内存存储为空
inMemory.size(); //> 0
// 内存存储中没有,会从磁盘中查找
inMemory.get(1); //> "one"
// 内存缓存的结果,它以后会过期并被移动到磁盘存储
inMemory.size(); //> 1
它也可以清空所有内存中的过期节点,把数据全部移动到磁盘存储中。
inMemory.put(1,11);
inMemory.put(2,11);
// 内存存储中的过期节点
inMemory.clearWithExpire();
TODO 过期计数的估计。map大小可以在短时间内超过限制。
TODO 修改监听者。
BTreeMap
BTreeMap
为MapDB提供了TreeMap
和TreeSet
。它是基于无锁并发的B-Link树。它对小key拥有良好的性能,并且拥有良好的竖直伸缩性。
TODO 解释压缩
TODO 描述 B-link树
参数
BTreeMap有一些可选参数,可以在构建maker时使用:最重要的就是序列化器。通常序列化器有一些推测和开销,所以想要更好的性能就需要使用特定的序列化器。指定键和值的序列器可以使用下面的代码。在 Serializer
接口中有一些静态字段可以用来做序列化器。
BTreeMap<Long, String> map = db.treeMap("map")
.keySerializer(Serializer.LONG)
.valueSerializer(Serializer.STRING)
.createOrOpen();
另一个有用的参数是大小计数器。默认情况下,BTreeMap不会追踪它的大小,map.size()
会进行一个线性扫描所有的节点去计数。你也可以启用大小计数器,这样的话, map.size()
就会立即返回,但是会在插入节点时有开销。
BTree 将其所有键和值存储为节点的一部分。节点的大小非常影响性能。一个大节点就意味着当搜索时必须反序列化许多键。一小节点加载很快,但是会让B树的层次变高,同时也会增加很多操作。默认节点大小为32个节点,可以通过下面方式进行更改。
BTreeMap<Long, String> map = db
.treeMap("map", Serializer.LONG, Serializer.STRING)
.counterEnable()
.createOrOpen();
值也被存储为BTree叶子节点的一部分。大值意味着巨额开销,因为当执行单个操作 map.get("key")
时,32个值被反序列化。但是只有一个值被返回。在这种情景下,最好的方式是把值储存到叶子节点以外。这样的话,叶子节点只有一个6字节的指针指向值的地址。
大值也可以为节省空间进行压缩。下面的例子就是将值存储到叶子节点之外,并且对每个值进行压缩:
BTreeMap<Long, String> map = db.treeMap("map")
.valuesOutsideNodesEnable()
.valueSerializer(new SerializerCompressionWrapper(Serializer.STRING))
.createOrOpen();
BTreeMap需要以某种方式对键进行排序。默认情况下,它依赖被大多数Java所实现的Comparable
接口。如果这个接口没有被实现,那么键序列化器就必须被提供。一个可以用来比较类数组的例子:
BTreeMap<Object[], Long> map = db.treeMap("map")
// 对于不知道的类,使用数组序列化器
.keySerializer(new SerializerArray())
// 或者可以对特定的类使用包装序列化器,比如 String
.keySerializer(new SerializerArray(Serializer.STRING))
.createOrOpen();
同样地,基础类型也可以作为键。可以使用 String
来代替 bytep[]
,这样能够提供更好的性能:
BTreeMap<byte[], Long> map = db.treeMap("map")
.keySerializer(Serializer.BYTE_ARRAY)
.valueSerializer(Serializer.LONG)
.createOrOpen();
键序列化器
BTreeMap拥有自己处理键的方式。让我以 Long
类型的键为例举例说明。
long类型的键序列化后占8个字节。为了减少空间使用,可以压缩这个值,使其更小。因此,数字10只占1个字节,300占2个字节,10000占三个字节等等。为了使键具有压缩性,我们需要使用更小的值来存储他们。因为这些键是有序的,所以可以使用delta压缩。这就是说只存储第一个值的完整形式,然后后面的值存储它们之间的差值。
另一个改进是让反序列化更迅速。通常情况下,TreeMap中存储的键是包装形式,比如 Long[]
。这将会有巨大的开销,因为每个键都需要有一个指针、类头等。而BTreeMap将存储这些键的基本类型数组 long[]
。最后如果键足够好,还可以使用 int[]
。因为数组拥有良好的内存局部性,在二分查找时会拥有巨大的性能提升。
对数字来说做这种优化比较简单。但是BTreeMap也适用于其他类型的键,比如 String
(通用前缀压缩,带偏移量的单字节数组), bytep[]
,UUID
, Date
等。
排序优化是自动实行的。你必须提升特定的键序列化器: .keySerializer(Serializer.LONG)
。
有一些选择和实现去打包这个键。可以通过查看序列化器类中的以 _PACK
结尾的静态字段来了解更多细节。
TODO 这是一个主要特性,文件细节和添加衡量标准。
数据抽取
TODO 数据抽取
碎片化
无锁设计的一个权衡是删除后的碎片化。但节点被移除变成空后,B-Linked-Tree不会删除这个节点。如果你先填满一个BTreeMap,然后再移除所有节点,那么大约40%的空间不会被释放。任何值(键保留)的更新都不会受到碎片化的影响。
这个碎片化不同于存储的碎片化,因此 DB.compact()
并不会有作用。一种解决办法是把所有的内容都移到一个新 BTreeMap
中。使用数据抽取流的方式非常迅速,新的Map将没有碎片化,并且拥有更好的节点局部性(理论上硬盘缓存友好)。
TODO 提供一个迁移BTreeMap内容的工具。TODO提供一个能够统计碎片化的方法。
在未来,我们将提供包装BTreeMap,它将能够自动进行这种压缩碎片化。它将使用三个集合:第一个 BTreeMap
将会设置为只读,并且包含这个数据。第二个小map将包含更新数据。第三个map将会周期性地合并前两个,然后再和主map交换。在Cassandra中的SSTable
和其他数据库工作方式就和这种工作方式类似。
前缀子map
使用数组作为键的MapDB提供了前缀子map。它是有间隔的,因此前缀子map是懒惰的,它不会加载所有的键。这里有一个基于 byte[]
为key的使用前缀的例子:
BTreeMap<byte[], Integer> map = db
.treeMap("towns", Serializer.BYTE_ARRAY, Serializer.INTEGER)
.createOrOpen();
map.put("New York".getBytes(), 1);
map.put("New Jersey".getBytes(), 2);
map.put("Boston".getBytes(), 3);
// 获取所有以 New开关的城市
Map<byte[], Integer> newCities = map.prefixSubMap("New".getBytes());
TODO 键序列化器必须为前缀子map提供 nextValue
。
复合键和元组
MapDB允许复合键使用 Object[]
形式。区间子map可以用来获取元组的子元组,或者创建一个简单形式的多重集map。Object数组是不可比较的,所以你必须提供一个有比较器特定的序列化器。这是一个使用Object[]形式创建 Map<Tuple3<String, String, Integer>, Double>
的例子,元组的第一个是城镇,第二个是街道,第三个是房子编号。 它拥有很多部分,源码在 github上。为了能够序列化和比较元组,可以使用 SerializerArrayTuple
,它把元组的各个部分作为构造器的参数:
// 初始化db和map
DB db = DBMaker.memoryDB().make();
BTreeMap<Object[], Integer> map = db.treeMap("towns")
.keySerializer(new SerializerArrayTuple(
Serializer.STRING, Serializer.STRING, Serializer.INTEGER))
.valueSerializer(Serializer.INTEGER)
.createOrOpen();
一但map被存入数据,我们就可以使用前缀子map来获取所有名称为Cong的城镇里的房子编号(城市是元组中的第一个元素):
// 获取所有名称为Cong的城镇里的房子编号(城市是元组中的第一个元素)
Map<Object[], Integer> cong =
map.prefixSubMap(new Object[]{"Cong"});
前缀子map相当于使用子map方法进行范围查询:
区间子map只能用来过滤左边的元素。为了获取中的元素,我们必须将子map和for循环结合起来:
cong = map.subMap(
new Object[]{"Cong"}, // 短数组是代表 “无穷小”
new Object[]{"Cong",null,null} // null 代表 “无空穷大
);
子map是可修改的,所以我们可以通过使用子map中的clear()方法来删除一个城市里的所有房子编号。
多重集Map
Multimap是一种一个键可以对应多个值的map。在Guava和Eclipse Collections中就有例子。它可能被写成 Map<Key, List<Value>>
,但是它在MapDB中不起作用。我们需要的键和值都是不可修改的,但是List并不是不可修改的。
有一个计划,打算在MapDB直接从Guava和EC中实现Multimap。截止目前,可以使用SortedSet结合元组和区间子集。这有一个例子,先构建Set,插入一些数据,然后使用键(元组的第一个元素)来获取多个值(元组的第二个元素):
// 初始化多重集 Map<String,List<Integer>>
NavigableSet<Object[]> multimap = db.treeSet("towns")
// 设置元组序列化器
.serializer(new SerializerArrayTuple(Serializer.STRING, Serializer.INTEGER))
.counterEnable()
.counterEnable()
.counterEnable()
.createOrOpen();
// 添加,键是元组(数组)的第一个元素,值是第二个
multimap.add(new Object[]{"John",1});
multimap.add(new Object[]{"John",2});
multimap.add(new Object[]{"Anna",1});
// 打印所有与John相关的值
Set johnSubset = multimap.subSet(
new Object[]{"John"}, // 低区间边界
new Object[]{"John", null}); // 高区间边界, null 是正无穷
TODO delta 打包元组
TODO MapDB将很快从Guava中实现multimap
和HTreeMap对比
BTreeMap更适合小键,比如数字和短字符串。
TODO 和HTreeMap比较。
复合键
BTreeMap可以使用复合键,复合键就是一个键拥有多个元素。范围查询可以获取与主元素相关的所有子元素。
下面是一个例子,将姓名(由姓和名组成)和年龄进行关联。我们可以查找出姓Smith(主元素)的所有人。
// 创建新map
BTreeMap<Tuple2, Integer> persons = db
.treeMap("persons", Tuple2.class, Integer.class)
.createOrOpen();
// 向map中插入三条Person的数据
persons.put(new Tuple2("Smith","John"), 45);
persons.put(new Tuple2("Smith","Peter"), 37);
persons.put(new Tuple2("Doe","John"), 70);
// 现在查询前缀是Smith的所有数据
NavigableMap<Tuple2,Integer> smiths =
persons.prefixSubMap(
new Tuple2("Smith", null) // null为范围查询的通配符
);
上面的示例,可以使用包装类和泛型进行更强类型化。在这里,使用类Surname
和类Firstname
。
// 创建新map
BTreeMap<Tuple2<Surname, Firstname>, Integer> persons = db
.treeMap("persons")
.keySerializer(new Tuple2Serializer()) // 特定的Tuple2序列化器
.valueSerializer(Serializer.INTEGER)
.createOrOpen();
// 向map中插入三条Person的数据
persons.put(new Tuple2(new Surname("Smith"),new Firstname("John")), 45);
persons.put(new Tuple2(new Surname("Smith"),new Firstname("Peter")), 37);
persons.put(new Tuple2(new Surname("Doe"),new Firstname("John")), 70);
// 现在查询前缀是Smith的所有数据
NavigableMap<Tuple2<Surname, Firstname>,Integer> smiths =
persons.prefixSubMap(
new Tuple2(new Surname("Smith"), null)
);
元组使用接口Comparable
作比较,所以所有的元素(Person
和Firstname
)都应该实现它。另一种选择是使用带有比较器的复合序列化器。例如,我们拥有这样的键Tuple2<byte[], byte[]>
,我们需要创建的序列化器是这种形式的:Tuple2Serializer(Serializer.BYTE_ARRAY, Serializer.BYTE_ARRAY)
。下面是代码示例:
BTreeMap<Tuple2<byte[], byte[]>, Integer> persons = db
.treeMap("persons")
.keySerializer(new Tuple2Serializer(Serializer.BYTE_ARRAY, Serializer.BYTE_ARRAY))
.valueSerializer(Serializer.INTEGER)
.createOrOpen();
persons.put(new Tuple2("Smith".getBytes(),"John".getBytes()), 45);
NavigableMap<Tuple2,Integer> smiths =
persons.prefixSubMap(
new Tuple2("Smith".getBytes(), null)
);
在上面的例子中,我们使用prefixSubMap(new Tuple2("surname", null))
该方法,它进行范围查询的方式是使用最小值和最大值替换第二个元素 。BTreeMap
中的这个方法不是标准的Map
方法,可以使用NavigableMap.subMap
来替换。
NavigableMap<Tuple2,Integer> smiths =
persons.prefixSubMap(new Tuple2("Smith", null));
// 等价于
smiths = persons.subMap(
new Tuple2("Smith", Integer.MIN_VALUE), true,
new Tuple2("Smith", Integer.MAX_VALUE), true
);
在上面的例子中,我们使用Integer
,因为它提供了最小值和最大值,为了使这个更简单,TupleSerializer
引入了特定的值来代表负无穷和正无穷,它甚至比最小值/最大值还小/大。null
代表负无穷,Truple.HI
代表正无穷。这两个值不可以序列化,也不可以存储到map中,但是可以用来作范围查询。
persons.subMap(
new Tuple2("Smith", null),
new Tuple2("Smith", Tuple.HI)
);
子map只能返回一个范围,也就是说,只能查询最左侧的元素。一个经常犯的错误是把无穷大放到中间,然后查询右侧的元素。下面的例子中元组有三个元素(姓,名和年龄),我们不能只查询姓和年龄,因为年龄是在最右侧的,它会被前面的无空大元素覆盖。
// 错误!! null在中间
persons.prefixSubMap(new Tuple3("Smith",null,11));
// 同子map
// 错误!! 无穷大在中间
persons.subMap(
new Tuple3("Smith", null, 11),
new Tuple3("Smith", Tuple.HI, 11)
);
固定长度的数组元组
元组可以被数组替换。这样的话,我们就没有了泛型,将会拥有很多类型转换。这是一个关于姓/名的例子。对于键序列化器,我们使用new SerializerArrayTuple(tupleSize)
。null
和Tuple.HI
将不再起作用,但是我们使用较短的数据作用前缀。
// 创建新map
BTreeMap<Object[], Integer> persons = db
.treeMap("persons", new SerializerArrayTuple(2), Serializer.INTEGER)
.createOrOpen();
// 向map中插入三条人的数据
persons.put(new Object[]{"Smith", "John"}, 45);
persons.put(new Object[]{"Smith", "Peter"}, 37);
persons.put(new Object[]{"Doe", "John"}, 70);
// 现在查询前缀是Smith的所有数据
NavigableMap<Object[],Integer> smiths =
persons.prefixSubMap(
new Object[]{"Smith"}
);
// TODO:null
为正无穷大,Tuple.HI
不存在。
可变长度的数组元组
MapDB也拥有通用的数组序列化器,它可用于当元组。这样的话,prefixSubmap
将不起作用。但是我可以使用子map:
// 创建新map
BTreeMap<Object[], Integer> persons = db
.treeMap("persons", new SerializerArrayDelta(), Serializer.INTEGER)
.createOrOpen();
// 向map中插入三条人的数据
persons.put(new Object[]{"Smith", "John"}, 45);
persons.put(new Object[]{"Smith", "Peter"}, 37);
persons.put(new Object[]{"Doe", "John"}, 70);
NavigableMap<Object[],Integer> smiths = persons.subMap(
new Object[]{"Smith"}, // 下限
new Object[]{"Smith", null} // 上限, 在这个序列化器中null代表正无穷
);
增量压缩
所有三个元组类型都可以使用增量压缩。
TODO 增量压缩