MapDB使用入门
背景
MapDB官网:http://www.mapdb.org
官方翻译之后的话:MapDB基于堆外存储、磁盘存储提供了Java的Maps、Sets、Lists、Queues等功能。它混合了Java集合框架和数据库引擎。它是基于Apache许可的免费的、开源的。
个人觉得:MapDB是一个轻量级的本地缓存的框架,它既可以使用对外存储,也可以使用磁盘存储(重启时数据不丢失)。它还提供事务的功能。
开发文档:https://jankotek.gitbooks.io/mapdb/content/quick-start/
开发机器配置:i5-9400 6c6t,32g内存,固态硬盘
MapDB入门实战
1、引入jar包
<!-- https://mvnrepository.com/artifact/org.mapdb/mapdb --> <dependency> <groupId>org.mapdb</groupId> <artifactId>mapdb</artifactId> <version>3.0.7</version> </dependency>
2、基于堆外存储的Hello,Simple
/** * 堆外内存map */ public static void offHeapMapTest1() { DB db = DBMaker.memoryDB().make(); ConcurrentMap map = db.hashMap("map").createOrOpen(); String key = "Hello"; String val = "simple"; map.put(key, val); System.out.println("第1次取值," + map.get(key)); }
执行结果:
--Hello,simple----
第1次取值,simple
2.1、插曲
刚开始执行的时候,总是报下面的异常
Exception in thread "main" java.lang.NoClassDefFoundError: org/eclipse/collections/impl/list/mutable/primitive/LongArrayList
at org.mapdb.StoreDirectAbstract.<init>(StoreDirectAbstract.kt:41)
at org.mapdb.StoreDirect.<init>(StoreDirect.kt:30)
at org.mapdb.StoreDirect$Companion.make(StoreDirect.kt:57)
at org.mapdb.StoreDirect$Companion.make$default(StoreDirect.kt:56)
at org.mapdb.DBMaker$Maker.make(DBMaker.kt:450)
at me.lovegao.mapdb.hello.HelloWorldDemo.offHeapMapTest1(HelloWorldDemo.java:18)
at me.lovegao.mapdb.hello.HelloWorldDemo.main(HelloWorldDemo.java:11)
Caused by: java.lang.ClassNotFoundException: org.eclipse.collections.impl.list.mutable.primitive.LongArrayList
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
我以为是jar包没加全,又加了Eclipse相关的jar包,发现还是这样。对着项目又是清理,又是重新编译,又是重新导包,发现都不行。
仔细看了maven的依赖,都是存在的,看MapDB的文档,也说上面那个配置包含全部的。
最后,索性把本地相关的jar包都删了,让maven重新下,最终正常了。
3、基于磁盘的Hello,Simple
基于磁盘存储的,为了保证数据的完整性,需要在关闭虚拟机前关闭DB。
public static void fileMapTest1() { DB db = DBMaker.fileDB("file.db").make(); ConcurrentMap map = db.hashMap("map").createOrOpen(); String key = "something"; String val = "here"; map.put(key, val); System.out.println("第1次取值," +map.get(key)); db.close(); System.out.println("----------重新打开----------"); db = DBMaker.fileDB("file.db").make(); map = db.hashMap("map").createOrOpen(); System.out.println("第2次取值," +map.get(key)); db.close(); }
执行结果:
--Hello,simple----
第1次取值,simple
----------重新打开----------
第2次取值,simple
结果符合预期。
3.1、基于磁盘的,内存映射的使用
/** * 在64位操作系统中,开启内存映射 * 个性化序列化 */ public static void fileMapMemoryMapTest() { DB db = DBMaker .fileDB("file.db") .fileMmapEnable() .make(); ConcurrentMap<String,Long> map = db .hashMap("mapsl", Serializer.STRING, Serializer.LONG) .createOrOpen(); long val = 51; map.put(DEMO_KEY, val); System.out.println("第1次取值,期望值:" + val + ",取到的值:" +map.get(DEMO_KEY)); db.close(); db = DBMaker .fileDB("file.db") .fileMmapEnable() .make(); map = db.hashMap("mapsl", Serializer.STRING, Serializer.LONG) .createOrOpen(); System.out.println("第2次取值,期望值:" + val + ",取到的值:" +map.get(DEMO_KEY)); db.close(); }
执行结果:
--Hello,simple----
第1次取值,期望值:51,取到的值:51
第2次取值,期望值:51,取到的值:51
4、性能对比
1)测试代码
package me.lovegao.mapdb.hello; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap; import org.mapdb.DB; import org.mapdb.DBMaker; import org.mapdb.Serializer; public class MapDBSpeedTest { private final static String DEMO_KEY = "Hello"; private final static String DEMO_VAL = "simple"; public static void main(String[] args) { System.out.println("--Hello,simple----"); // fileMapMemoryMapTest(); mapTest(); } public static void mapTest() { int dataNum = 10000; List<DB> dbList = new ArrayList(); Map<String, Map<String, Long>> testMap = new HashMap(); Map<String, Long> dataMap = generateTestData(dataNum); //java原生-堆内map ConcurrentMap<String, Long> inHeapDbMap = new ConcurrentHashMap(); testMap.put("原生map", inHeapDbMap); //堆外map DB offHeapDb = DBMaker.memoryDB().make(); dbList.add(offHeapDb); ConcurrentMap offHeapDbMap = offHeapDb.hashMap("map").createOrOpen(); testMap.put("堆外map", offHeapDbMap); //基于磁盘map DB fileDb = DBMaker.fileDB("file1.db").make(); dbList.add(fileDb); ConcurrentMap<String,Long> fileDbMap = fileDb .hashMap("map1", Serializer.STRING, Serializer.LONG) .createOrOpen(); testMap.put("基于磁盘map", fileDbMap); //基于磁盘-内存映射map DB fileMmapDb = DBMaker .fileDB("file2.db") .fileChannelEnable() //By default MapDB uses RandomAccessFile to access disk storage. Outside fast mmap files there is third option based on FileChannel. It should be faster than RandomAccessFile .fileMmapEnable() // Always enable mmap // .fileMmapEnableIfSupported() // Only enable mmap on supported platforms,对性能影响较大 .fileMmapPreclearDisable() // Make mmap file faster // .allocateStartSize( 10 * 1024*1024*1024) // 10GB,初始容量 // .allocateIncrement(512 * 1024*1024) // 512MB,每次增加容量 .make(); //optionally preload file content into disk cache fileMmapDb.getStore().fileLoad(); dbList.add(fileMmapDb); ConcurrentMap<String,Long> fileMmapMap = fileMmapDb .hashMap("map2", Serializer.STRING, Serializer.LONG) .createOrOpen(); testMap.put("基于磁盘-内存映射map", fileMmapMap); System.out.println("-----------put---数据量:"+dataNum+"------"); for(String mapType : testMap.keySet()) { putGetMapTest(mapType, testMap.get(mapType), dataMap, true); } System.out.println("-----------------------------------------\n"); System.out.println("-----------get---数据量:"+dataNum+"------"); for(String mapType : testMap.keySet()) { putGetMapTest(mapType, testMap.get(mapType), dataMap, false); } for(DB db : dbList) { db.close(); } } /** * putGet测试 * @param map * @param dataMap * @param put * @return <耗时, 异常数> */ public static TwoTuple<Long, Long> putGetMapTest(String mapType, Map<String, Long> map, Map<String, Long> dataMap, boolean put) { long useTime = 0L; long errorNum = 0L; Iterator<Entry<String, Long>> entryIt = dataMap.entrySet().iterator(); while(entryIt.hasNext()) { Entry<String, Long> entry = entryIt.next(); if(put) { long t1 = System.nanoTime(); map.put(entry.getKey(), entry.getValue()); useTime = System.nanoTime() - t1; } else { long t1 = System.nanoTime(); long val = map.get(entry.getKey()); useTime = System.nanoTime() - t1; if(val != entry.getValue()) { errorNum++; } } } double avgUseTime = (double)useTime / dataMap.size(); String fmtStr = "map类型:%s,总耗时:%dns,平均耗时%ens,异常数量:%d"; System.out.println(String.format(fmtStr, mapType, useTime, avgUseTime, errorNum)); return new TwoTuple<Long, Long>(useTime, errorNum); } /** * 生成测试数据 * @param size * @return */ public static Map<String, Long> generateTestData(int size) { Map<String, Long> map = new HashMap(); int arrLength = 26; char[] words = new char[arrLength]; for(int i=0; i<arrLength; i++) { words[i] = (char) ('a' + i); } System.out.println(words); String demoWord = new String(words); for(int i=0; i<size; i++) { String key = demoWord.substring(i%arrLength, i%arrLength) + i; long val = i; map.put(key, val); } return map; } /** * 对外内存map */ public static void offHeapMapTest1() { DB db = DBMaker.memoryDB().make(); ConcurrentMap map = db.hashMap("map").createOrOpen(); map.put(DEMO_KEY, DEMO_VAL); System.out.println("第1次取值," + map.get(DEMO_KEY)); } /** * 基于磁盘的存储 */ public static void fileMapTest1() { DB db = DBMaker.fileDB("file.db").make(); ConcurrentMap map = db.hashMap("map").createOrOpen(); map.put(DEMO_KEY, DEMO_VAL); System.out.println("第1次取值," +map.get(DEMO_KEY)); db.close(); System.out.println("----------重新打开----------"); db = DBMaker.fileDB("file.db").make(); map = db.hashMap("map").createOrOpen(); System.out.println("第2次取值," +map.get(DEMO_KEY)); db.close(); } /** * 在64位操作系统中,开启内存映射 * 个性化序列化 */ public static void fileMapMemoryMapTest() { DB db = DBMaker .fileDB("file.db") .fileMmapEnable() .make(); ConcurrentMap<String,Long> map = db .hashMap("mapsl", Serializer.STRING, Serializer.LONG) .createOrOpen(); long val = 51; map.put(DEMO_KEY, val); System.out.println("第1次取值,期望值:" + val + ",取到的值:" +map.get(DEMO_KEY)); db.close(); db = DBMaker .fileDB("file.db") .fileMmapEnable() .make(); map = db.hashMap("mapsl", Serializer.STRING, Serializer.LONG) .createOrOpen(); System.out.println("第2次取值,期望值:" + val + ",取到的值:" +map.get(DEMO_KEY)); db.close(); } }
2)测试结果
map类型 | 测试数据量 | 测试类型 | 总耗时(ns) | 平均耗时(ns) |
原生map | 10000 | put | 100 | 1.000000e-02 |
堆外map | 同上 | put | 4800 | 4.800000e-01 |
基于磁盘map | 同上 | put | 345000 | 3.450000e+01 |
基于磁盘-内存映射map | 同上 | put | 6000 | 6.000000e-01 |
原生map | 同上 | get | 100 | 1.000000e-02 |
堆外map | 同上 | get | 2000 | 2.000000e-01 |
基于磁盘map | 同上 | get | 75400 | 7.540000e+00 |
基于磁盘-内存映射map | 同上 | get | 1100 | 1.100000e-01 |
3)结论
①原生的基于堆的map速度始终是最快的
②堆外map和基于磁盘且开启了内存映射的map相比,优势较小。至于原因,有待深入理解。
③对于“基于磁盘-内存映射map”,使用“fileMmapEnableIfSupported”配置,对性能影响较大,建议直接开启。配置“fileMmapPreclearDisable”对于put的性能提升较大(约一倍提升)。
MapDB事务
MapDB是支持事务的,具体使用如下:
package me.lovegao.mapdb.hello; import java.util.concurrent.ConcurrentMap; import org.mapdb.DB; import org.mapdb.DBMaker; import org.mapdb.Serializer; public class MapDBTransaction { public static void main(String[] args) { DB db = DBMaker .fileDB("file3.db") .fileMmapEnable() .transactionEnable() .closeOnJvmShutdown() //JVM关闭时关闭db .make(); ConcurrentMap<String,Long> map = db .hashMap("mapsl3", Serializer.STRING, Serializer.LONG) .createOrOpen(); map.put("a", 1L); map.put("b", 2L); db.commit(); System.out.println(map.get("a")); System.out.println(map.get("b")); map.put("c", 3L); System.out.println("rollback之前,c:" + map.get("c")); db.rollback(); System.out.println("rollback之后,a:" + map.get("a")); System.out.println("rollback之后,c:" + map.get("c")); } }
运行结果:
1
2
rollback之前,c:3
rollback之后,a:1
rollback之后,c:null
因为配置了closeOnJvmShutdown,所以再次运行时能够正常运行。
如果去掉了transactionEnable和closeOnJvmShutdown,再次运行时将出现以下异常:
Exception in thread "main" org.mapdb.DBException$DataCorruption: Header checksum broken. Store was not closed correctly and might be corrupted. Use `DBMaker.checksumHeaderBypass()` to recover your data. Use clean shutdown or enable transactions to protect the store in the future.
at org.mapdb.StoreDirectAbstract.fileHeaderCheck(StoreDirectAbstract.kt:113)
at org.mapdb.StoreDirect.<init>(StoreDirect.kt:114)
at org.mapdb.StoreDirect$Companion.make(StoreDirect.kt:57)
at org.mapdb.StoreDirect$Companion.make$default(StoreDirect.kt:56)
at org.mapdb.DBMaker$Maker.make(DBMaker.kt:450)
at me.lovegao.mapdb.hello.MapDBTransaction.main(MapDBTransaction.java:17)
最后说以下,fileDB("file3.db")这里的路径可以指定其他目录,默认是在项目的根目录下。
路径是自己创建的,文件是MapDB自动创建的,切记不要多此一举,把文件也创建了,那样会报错的。