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库和其他一些库。这里有它所有的版本。
以下是一个简单的示例。在内存中打开一个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();
快速提示
map.put()
快得多.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为mapBD提供对hashMap 和hashSet 的集合。它可以用作缓存并且支持数据过期失效。它是线程安全的,支持并发更新操作。
它是线程安全的,采用分段技术支持并行写入,每一个分段都有一个readwritelock锁。和JDK7中ConcurrentHashMap的原理类似。
HTreeMap 是一个分段的hash树,和其他的HashMaps不同,它不使用固定大小的Hash表,也不会在数据增长时刷新整个Hash表。HtreeMap使用的是自动扩展索引树,因此不需要调整大小。它也占用更少的空间,因为空的散列槽不占用任何空间。另一方面,树结构需要更多的搜索,访问速度较慢。它的性能随着大小而下降到什么程度?.
HTreeMap 可根据四个条件选择 entry 过期:最大Map的大小,最大存储大小,上一次修改时间和最后一次访问时间。过期的entries 会自动删除,采用的是FIFO队列,每一个分段都有自己独立的到期队列。
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值是使用的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的设计是基于函数设计的。它需要三个参数:
segment * node size ^ level count
默认的最大哈希表大小为8*16^4=50万个。另一个参数就是集合大小计数器。默认情况下,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为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();
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());