zoukankan      html  css  js  c++  java
  • mapDB文档

    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像乐高一样是可插拔的。DBDBMaker 这两个类像胶水一样连接着不同的部分。

    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提供了 HashMapHashSet。它支持数据过期,可以用来当缓存使用。它是线程安全的,支持并行更新扩展操作。

    它是线程是安全的,使用分段技术支持并行写,每一段都是一个读写锁。这和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方法控制的。它拥有三个参数:

    1. 并发数,分段的数量。默认为8,它总是会被取整到2的幂。
    2. 索引树直接节点的最大的节点数量。默认为16,它总是会被取整到2的幂,最大值为128个节点。
    3. 索引树的高度。默认为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提供了TreeMapTreeSet。它是基于无锁并发的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[]UUIDDate等。

    排序优化是自动实行的。你必须提升特定的键序列化器: .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。在GuavaEclipse 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作比较,所以所有的元素(PersonFirstname)都应该实现它。另一种选择是使用带有比较器的复合序列化器。例如,我们拥有这样的键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)nullTuple.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 增量压缩

    未完待续。。。

  • 相关阅读:
    494 Target Sum 目标和
    493 Reverse Pairs 翻转对
    492 Construct the Rectangle 构建矩形
    491 Increasing Subsequences 递增子序列
    488 Zuma Game 祖玛游戏
    486 Predict the Winner 预测赢家
    485 Max Consecutive Ones 最大连续1的个数
    483 Smallest Good Base
    Django Form组件
    Django Auth组件
  • 原文地址:https://www.cnblogs.com/dwtfukgv/p/15375702.html
Copyright © 2011-2022 走看看