当前位置: 首页 > 工具软件 > MapDB > 使用案例 >

MapDB中文文档

洪建茗
2023-12-01

介绍

MapDB是一个基于(Apache 2.0 licensed)开源的Java嵌入式数据库引擎和集合框架。他支持针对Map,Set,Queues,Bitmaps 的范围查询,数据过期,压缩,堆外存储和流的操作。MapDB可能是Java最快的数据库,性能就像操作java中的集合一样。他也支持一些高级功能如:ACID事务,快照和增量备份等。
本手册正在进行中,将与MapDB3.0版本一起完成。我希望你会发现它有用。如果你想为mapDB做贡献,我们很乐意接收。本手册中的示例代码都在github上。

快速开始

快速介绍

mapdb很灵活,有很多配置选项。但在大多数情况下,它只配置了几行代码。

获得

MapDB的maven仓库

<dependency>
    <groupId>org.mapdb</groupId>
    <artifactId>mapdb</artifactId>
    <version>VERSION</version>
</dependency>

你可以从这里获得最新的版本 https://mvnrepository.com/artifact/org.mapdb/mapdb
maven 3.0.7 日常的快照版本地址在这里

<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>

你也可以直接下载mapDB的 jar文件
mapdb也可以依赖于eclipse、Guava、kotlin库和其他一些库。这里有它所有的版本

Hello World

以下是一个简单的示例。在内存中打开一个HashMap ,可以使用堆外存储,不受垃圾收集的限制。

//import org.mapdb.*
DB db = DBMaker.memoryDB().make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");

HashMap或者是其他的集合也可以存在在文件中。在这种情况下,可以再java重启时保存数据。这里必须使用DB.close() 来保护文件数据不被损坏。另一种方式是启用带有写日志的事务。

DB db = DBMaker.fileDB("file.db").make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");
db.close();

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();

快速提示

  • 在64位系统上,使用内存文件映射速度更快,性能更好。
  • mapdb有用于快速批量导入集合的pump。它比map.put()快得多.
  • 事务的开启会影响性能,但是没有他可能会存在文件数据损坏的情况。
  • 存储在mapdb中的数据(键和值)应该是不可变的,mapdb的对象序列化是在后台完成的

DB

DB 和 DBMaker

mapdb就像乐高一样,可以插拔。这里有两个类 DB和DBMaker来充当这样的角色。
DBMaker这个类来处理数据的配置,创建和开启。mapdb有多重模式和配置,大多都可以通过dbMaker来实现。
一个DB 实例表示一个打开的数据库(或者一个单一的事务会话)。它也可以用来创建或打开集合存储。也能够使用一些方法如: commit(),rollback(), close() 来处理数据库的生命周期 。
可以使用很多如DBMaker.fileDB() 类似这样的*DB的静态方法来打开或创建一个存储。MapDB有很多格式和模式。每一个xxxDB() 使用的不同的模式,比如说:memoryDB()在内存中使用的byte[]数组。appendFileDB() 使用的是append日志文件,等等。
一个xxxDB()方法后跟一个或多个配置选项,最后是一个make()方法,该方法应用所有选项,打开选定的存储并返回一个db对象。

DB db = DBMaker
        .fileDB("/some/file")
        //TODO encryption API
        //.encryptionEnable("password")
        .make();

打开和创建一个集合

一旦你有数据库,你可以打开一个集合或其他记录。数据库使用生成器样式配置。它从集合类型(hashmap,treeset…)和名称开始,然后应用配置,最后是操作指示器。
此示例打开(或创建)名为“example”的TreeSet

NavigableSet treeSet = db.treeSet("example").createOrOpen();

你也可以使用其他的配置方式

NavigableSet<String> treeSet = db
        .treeSet("treeSet")
        .maxNodeSize(112)
        .serializer(Serializer.STRING)
        .createOrOpen();

这里有三种不同的创建方法:

  • create() 创建一个新的集合,如果集合存在则抛出异常。
  • open() 打开一个集合,如果这个集合不存在则抛出异常。
  • createOrOpen() 如果集合存在就打开,如果不存在就创建一个。

DB不仅限于集合,还可以创建其他类型的记录,如原子记录:

Atomic.Var<Person> var = db.atomicVar("mainPerson",Person.SERIALIZER).createOrOpen();

事务

DB 有一些方法来处理事务:commit(), rollback() 和 close()
一个DB对象代表一个单一事务

ConcurrentNavigableMap<Integer,String> map = db
        .treeMap("collectionName", Serializer.INTEGER, Serializer.STRING)
        .createOrOpen();

map.put(1,"one");
map.put(2,"two");
//没提交前,map中的key值集合[1,2]

db.commit();  //提交

map.put(3,"three");
//现在的key值结合是[1,2,3]
db.rollback(); //事务回滚
//map中的key值集合[1,2]
db.close();

HTreeMap

htreemap为mapBD提供对hashMap 和hashSet 的集合。它可以用作缓存并且支持数据过期失效。它是线程安全的,支持并发更新操作。
它是线程安全的,采用分段技术支持并行写入,每一个分段都有一个readwritelock锁。和JDK7中ConcurrentHashMap的原理类似。
HTreeMap 是一个分段的hash树,和其他的HashMaps不同,它不使用固定大小的Hash表,也不会在数据增长时刷新整个Hash表。HtreeMap使用的是自动扩展索引树,因此不需要调整大小。它也占用更少的空间,因为空的散列槽不占用任何空间。另一方面,树结构需要更多的搜索,访问速度较慢。它的性能随着大小而下降到什么程度?.
HTreeMap 可根据四个条件选择 entry 过期:最大Map的大小,最大存储大小,上一次修改时间和最后一次访问时间。过期的entries 会自动删除,采用的是FIFO队列,每一个分段都有自己独立的到期队列。

Serializers

htreemap有许多参数。最重要的是name,它标识db对象内的映射和处理映射内数据的序列化程序

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

大多数的Hash值是使用的object.hashcode()生成的32位hash值,并且使用object.equals()来检查相等。
然而很多类(byte[], int[] ) 都没有实现它。
MapBD使用的是序列化后的Key来生成Hash值并且进行比较。如果Key使用了Serializer.BYTE_ARRAY 那么byte[] 数组是可以直接作为HtreeMap的Key.

HTreeMap<byte[], Long> map = db.hashMap("map")
        .keySerializer(Serializer.BYTE_ARRAY)
        .valueSerializer(Serializer.LONG)
        .create();

在某些类中的弱Hashcode() 会存在一些问题,导致冲突并降低性能。String.hashCode() 是个弱哈希,但它是规范的一部分,无法改变。jdk中的hashmap以牺牲内存和性能开销为代价实现了许多解决方案。htreemap没有这样的解决方案,而弱散列将大大降低它的速度。相反,htreemap正在修复问题的根源,serializer.string使用更强的xxhash,从而生成更少的冲突。string.hashcode()仍然可用,但具有不同的序列化程序

//这里将使用强的XXHash
HTreeMap<String, Long> map = db.hashMap("map")
        // 默认使用强 XXHash
        .keySerializer(Serializer.STRING)
        .valueSerializer(Serializer.LONG)
        .create();

//使用弱 `String.hashCode()`
HTreeMap<String, Long> map2 = db.hashMap("map2")
        // 使用 String.hashCode()
        .keySerializer(Serializer.STRING_ORIGHASH)
        .valueSerializer(Serializer.LONG)
        .create();

哈希映射易受哈希冲突攻击。HtreeMap对散列种子进行了保护。它是在创建集合并与其定义一起持久化时随机生成的。用户还可以提供自己的散列种子:

HTreeMap<String, Long> map = db
        .hashMap("map", Serializer.STRING, Serializer.LONG)
        .hashSeed(111)  //force Hash Seed value
        .create();

设计

hashmap有初始容量、负载系数等参数。mapdb有不同的参数集来控制其访问时间和最大大小。
并发性是通过多个分段来实现的,每个分段都有单独的读写锁。每个并发分段都是独立的,它有自己的大小计数器、迭代器和过期队列。分段数是可以配置的。太小的数目将导致并发更新的拥塞,太大的数目将增加内存开销。
HTreeMap 使用的是索引树而不是Object[] 的增长的哈希表。索引树是一种稀疏的数组结构,它使用数组的树层次结构,它是稀疏的,所以未使用的条目不会占用任何空间。也不进行hash刷新(将所有条目复制到更大的数组),但也不能超出其初始容量。HtreeMap的设计是基于函数设计的。它需要三个参数:

  • 并发,段数。默认值是8,它总是四舍五入为2的幂
  • 索引数的直接节点的最大值,默认值是16,它总是四舍五入为2的幂。最大值为128个条目。
  • 索引树中的级别数,默认值为4
    最大哈希表大小计算为 segment * node size ^ level count 默认的最大哈希表大小为8*16^4=50万个。
    如果哈希表的大小设置得太低,则一旦哈希表填满,将开始发生哈希冲突,性能将降低。即使哈希表已满,htreemap也将接受新的,但性能将降低。
    32位哈希对哈希表大小施加了上限:40亿个

其他参数

另一个参数就是集合大小计数器。默认情况下,htreemap不跟踪其大小,而map.size()执行全局扫码来统计数目的。您可以启用大小计数器,在这种情况下,map.size()是实时的,但是在插入时会有一些开销。

HTreeMap<String, Long> map = db
        .hashMap("map", Serializer.STRING, Serializer.LONG)
        .counterEnable()
        .create();

最后,这里有一个值加载程序,它是一个函数,在没有找到现有键的情况下加载一个值。一个新的键/值将插入到映射中,这种方法map.get(key) 从不返回控制,这主要使用与各种缓存和生成器

HTreeMap<String,Long> map = db
        .hashMap("map", Serializer.STRING, Serializer.LONG)
        .valueLoader(s -> 1L)
        .create();

//key不存在,这里 返回 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 配置选项跟DB创建的HtreeMap类似。但是Sharded HTreeMap没有DB关联的对象,所以在要关闭它的时候,必须直接调用HtreeMap.close()方法。

过期

HtreeMap支持数据过期,有一下规则:

  • 一条记录存在的时间比过期的时间长。这个有效期可以是创建时间,或者最后一次修改时间或访问时间。
  • 总记录数超过集合的最大容量
  • 存储容量超过最大的限制。

过期时间是创建时间或最后一次修改时间或访问时间:

// 上次修改10分钟后 删除
// 或 上次获取后1分钟 删除
HTreeMap cache = db
        .hashMap("cache")
        .expireAfterUpdate(10, TimeUnit.MINUTES)
        .expireAfterCreate(10, TimeUnit.MINUTES)
        .expireAfterGet(1, TimeUnit.MINUTES)
        .create();

创建一个16G容量的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();

//once we are done, background threads needs to be stopped
db.close();

过期策略可以和Sharded HTreeMap 结合使用,可以获得更好的并发性。在这种情况下,每个段都有独立的存储,这提高了并行更新下的可伸缩性。

HTreeMap cache = DBMaker
        .memoryShardedHashMap(16)
        .expireAfterUpdate()
        .expireStoreSize(128*1024*1024)
        .create();

Sharded HTreeMap 也支持使用线程池的方式,随时时间的推移,存储空间的数据碎片化会越来越多,如果有太多的空余空间,可以选择计划定期压缩,因为每一个存储都是单独压缩的,所以压缩不会影响正在运行的线程。

HTreeMap cache = DBMaker
        .memoryShardedHashMap(16)
        .expireAfterUpdate()
        .expireStoreSize(128*1024*1024)

        //起三个线程来移除过期数据
        .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");

//first remove from inMemory
inMemory.remove("key");
onDisk.get("key"); // -> not found

如果调用了inmemory.get(key),但值不存在,则值加载器将尝试在磁盘中查找映射。

onDisk.put(1,"one");    //磁盘中有,内存中没有
inMemory.size();        //> 0
// get 方法在内存没有获取到,就会从磁盘中获取
inMemory.get(1);        //> "one"
//内存中现在有了,但是一会它将别移到磁盘中去
inMemory.size();  

也可以清除整个主映射并将所有数据移动到磁盘

inMemory.put(1,11);
inMemory.put(2,11);
//expire entire content of inMemory Map
inMemory.clearWithExpire();

BTreeMap

BTreeMap为MapDB提供TreeMap和TreeSet集合操作,它是基于无锁并发的B-连接树
它为比较小的key提供了很好的性能,并且具有良好的垂直扩展性。

参数

BTreeMap 可以通过maker来指定一些可选参数。最重要的就是序列化了。一般的序列化是需要额外消耗资源的,因此需要一些特定的序列化程序来提高性能。指定的键值序列化可以使用一下面代码,序列化程序接口上有几十个可用作静态字段的现成序列化程序:

BTreeMap<Long, String> map = db.treeMap("map")
        .keySerializer(Serializer.LONG)
        .valueSerializer(Serializer.STRING)
        .createOrOpen();

另一个有用的参数就是 大小计数器。默认情况下,BTreeMap不会跟踪大小,调用map.size()会扫描整个集合。如果你启动大小计数器,那么map.size是实时统计的,但是会影响插入的效率。
BTree将其所有的键和值存储为BTree节点的一部分。节点的大小对性能的影响很大,大节点就意味着在查找时需要反序列化更多的键。一个小的节点加载速度更快,但是深度更深,并且需要更多的操作。
节点默认大小是32.并且可以通过以下方法来更改:

BTreeMap<Long, String> map = db
        .treeMap("map", Serializer.LONG, Serializer.STRING)
        .counterEnable()
        .createOrOpen();

值也作为Btree的叶子节点的一部分存储,大的值意味着更多的开销,如单独一个map.get(“key”) 有32个值被反序列化,但只返回一个值。在这种情况下,最好将值存储在叶节点之外的单独记录中。在这种情况下,叶节点只有一个指向该值的6字节recid。也可以压缩大值以节省空间。此示例将值存储在btree 叶子节点之外,并对每个值应用压缩:

BTreeMap<Object[], Long> map = db.treeMap("map")
        // use array serializer for unknown objects
        .keySerializer(new SerializerArray())
        // or use wrapped serializer for specific objects such as String
        .keySerializer(new SerializerArray(Serializer.STRING))
        .createOrOpen();

也可以使用基元数组作为键。可以用byte[]替换字符串,这直接导致更好的性能:

BTreeMap<byte[], Long> map = db.treeMap("map")
        .keySerializer(Serializer.BYTE_ARRAY)
        .valueSerializer(Serializer.LONG)
        .createOrOpen();

Key序列化

BTreeMap的性能取决于处理Key的方式,用一个长key来说明这一点。
序列化后长Key占8个字节。需要最小化空间使用,可以将其打包到更小。如数字10将占据一个单一的字节,300占用2个字节,10000占用三个等等。为了更好的对key进行打包,我们需要使用最小量存储。所以对于key的存储,我们使用了增量压缩,它将完整的存储第一个值,然后仅存储数据之间的差异值。
另一个改进是加快反序列化的速度。在正常的TreeMap中,key的存储形式是一种包装的表,例如:Long[].
这就有很大的额外开销,因为每一个key都需要一个新的指针,类头。BTreeMap将key存储到一个long[] 的原始数组。如果键足够小,甚至可以存放在int[]里。因为数组具有更好的内存结构,因此二进制搜索的性能有了很大的提高。
对数字进行优化很简单,但是BTreeMap也支持其他类型的key,如String,Byte[],UUID,Date等。这些key的优化是自动使用的,你需要使用专用的序列化方式,如.keySerializer(Serializer.LONG)

碎片化

前缀映射

对于基于数字的键(元组、字符串或数组)MapDB 提供前缀映射。它使用间隔,所以前缀submap是惰性的,它不会加载所有键。示例:

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);

//get all New* cities
Map<byte[], Integer> newCities = map.prefixSubMap("New".getBytes());

组合键和元组

未完待续。。。

 类似资料: