探索Map<D,B>:Java中的高性能内存数据存储解决方案
MapDB
MapDB 是一个快速、易用的嵌入式 Java 数据库引擎,它提供了基于磁盘或者堆外(off-heap 允许Java 直接操作内存空间, 类似于C 的malloc 和free)存储的并发的Maps、Sets、Queues。MapDB 的前身是JDBM,已经有15 年的历史。MapDB 支持ACID 事务、MVCC 隔离,它的jar 包只有200KB,且无其它依赖,非常轻量。MapDB 目前的版本是1.0.5,相对来说功能已经稳定,并有全职的开发者支持开发。
MapDB 全部使用 Java 编写,支持 100GB 以上的数据存储,且性能可以与很多由 C 语言开发的数据库(谷歌的 Leveldb、甲骨文的 Berkeley DB)相媲美。它的主要特性如下:
- 并发。MapDB 包含记录级别的锁和先进的并发控制引擎,它的性能可以在多核之间线性扩展,支持并发写。
- 快速。MapDB 的性能可以与原生数据库相媲美,它经过多次的优化以及重写。
- ACID 事务。支持 ACID 事务并实现了不同形式的 MVCC,MapDB 使用 write-ahead-log 或者 append-only 的方式来存储操作日志。
- 灵活。MapDB 可以运行在内存缓存中,也可以支持 TB 级别的的数据库。它支持各种配置以满足不同的需求。
- Hackable。很多特性(实例缓存 cache、异步写、压缩) 都是一组类, 易于加入新功能和组件。
- SQL Like。MapDB 有非常快的 SQL 引擎,包含很多从关系型数据库移植过来的特性,比如辅助索引 / 集合、自增序列 ID、 连接、触发器、组合键。
- 低磁盘使用率。MapDB 的能缩小磁盘的使用量,并且压缩以及序列化过程都非常快速。
MapDB 采用模块化的架构设计,非常容易扩展,每一个模块都可以被关掉,并且每个模块都可以有不同的设计,比如 MapDB 中有 5 种不同的缓存以及 3 种不同的存储模式。
CodeFutures 的 CEO Cory解释了 MapDB 所要解决的问题,“MapDB 为 Java 程序员提供了一种自然的方式来快速存储大对象,它可以精确匹配应用的需求。大部分应用都遇到过内存溢出或者很多的对象被装载到 JVM 而引起的过度垃圾回收的问题,很多时候这些问题是由于应用中有很多大的集合对象造成的。现在你可以使用 MapDB 来处理这些大的集合,且连 API 都不需要改。另外,MapDB 可以轻松的实现排序、遍历、事务。”
另外,结合 SSD 硬盘,MapDB 可以用于某些单节点的大数据场景。当数据集没有大到使用 Hadoop 处理时,可以考虑使用使用 MapDB 来编写基于内存的处理程序。
可以看到官网的介绍
MapDB提供了Java Maps,Sets,LIsts,Queues和其它堆外或磁盘存储支持的集合。它是java集合框架和嵌入式数据库引擎的混合体。它是在 Apache 许可下免费且开源的。
我们点击DOC,查看它的官方文档。
文档提供了三种格式
- Git Books
- ebook
并且所有文档的代码案例都上传到了Github仓库。Github repository.
我们点击查看Git Books的官方文档。
这里又介绍了以下MapDB
MapDB 可能是速度最快的 Java 数据库,其性能可与 java.util 集合相媲美。它还提供 ACID 事务、快照、增量备份等高级功能。
快速开始
MapDB 非常灵活,有很多配置选项。但在大多数情况下,只需几行代码就能完成配置。
MapDB也已经上传到了Maven中央仓库,通过xml文件就可以引用了。
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>VERSION</version>
</dependency>
Maven Repository: org.mapdb » mapdb (mvnrepository.com)
我们可以看到目前MapDB的最新版本为3.1.0
Hello World
这里给出了一个简单的示例,它打开内存中的 HashMap,使用堆外存储,不受垃圾回收的限制。
我们创建一个项目并且导入依赖
<!-- https://mvnrepository.com/artifact/org.mapdb/mapdb -->
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
public void test1() {
DB db = DBMaker.memoryDB().make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");
System.out.println(map.get("something"));
}
我们存储了一个something
的key,value为here
到Map中。这里的数据是存储在内存中的,所有程序停止运行就没有了。
HashMap 和其他集合也可以存储在文件中。在这种情况下,内容可以在 JVM 重启时保留。有必要调用 DB.close(),以防止文件数据损坏。另一种方法是启用提前写日志的事务。
public void test2(){
DB db = DBMaker.fileDB("file.db").make();
ConcurrentMap map = db.hashMap("map").createOrOpen();
map.put("something", "here");
System.out.println(map.get("something"));
db.close();
}
这时候我们运行程序,可以看到也成功拿到了对应的数据。并在程序目录下生成了一个file.db
文件。里面存储的就是我们序列化后的数据。
默认情况下,MapDB 使用通用序列化,可以序列化任何数据类型。使用专门的序列化程序会更快、更节省内存。此外,我们还可以在 64 位操作系统上启用更快的内存映射文件(只用用于数据存储在文件中fileDB,不能用于memoryDB):
public void test3() {
DB db = DBMaker
.fileDB("file.db")
//64 位操作系统上启用更快的内存映射文件
.fileMmapEnable()
.make();
ConcurrentMap<String, Long> map = db
//指定专门的序列化方式
.hashMap("map", Serializer.STRING, Serializer.LONG)
.createOrOpen();
map.put("something", 111L);
System.out.println(map.get("something"));
db.close();
}
DB and DBMaker
可以从Hello World的例子中看到这两个类。那么这两个类分别是什么呢?我们一起来看官网学习吧。
MapDB 像乐高积木一样是可插拔的。有两个类充当不同部分之间的粘合剂,即 DBMaster
和 DB
类。
- DBMaster 类处理数据库配置、创建和打开。 MapDB 有多种模式和配置选项。其中大部分可以使用此类进行设置。
- DB 实例代表已打开的数据库(或单个事务会话)。它可用于创建和打开集合存储。它还可以使用 commit()、rollback() 和 close() 等方法处理数据库的生命周期。
要打开(或创建)一個储存库,使用 DB 静态方法之一,如 DBMaker.fileDB()。
MapDB 有更多格式和模式,因此每个 xxxDB() 使用不同的模式:
- memoryDB():打开一个由字节[]数组支持的内存数据库
- appendFileDB():打开一个只使用追加日志文件的数据库,等等。
我们可以查看源码,查看所支持的全部模式。
在 xxxDB() 方法之后是一个或多个配置选项,最后是 make() 方法,该方法应用所有选项,打开选定的存储并返回 DB 对象。
官网提供了一个示例将打开一个已启用加密的文件存储:
DB db = DBMaker
.fileDB("/some/file")
//TODO encryption API
//.encryptionEnable("password")
.make();
Open and create collection
Open是打开的意思,这里代表的更多的是使用。即使用一个已经创建好的collection集合。
一旦您拥有数据库,您就可以打开集合或其他记录。 DB 使用构建器样式配置。它以集合类型(hashMap、treeSet...)和名称开头,然后是应用配置,最后是操作指示器。
此示例打开(或创建新的)名为“example”的 TreeSet
NavigableSet treeSet = db.treeSet("example").createOrOpen();
当然我也可以添加更多的配置项:
NavigableSet<String> treeSet = db
.treeSet("treeSet")
.maxNodeSize(112)
.serializer(Serializer.STRING)
.createOrOpen();
- 最大的个数为112
- 指定序列化方式为String
创建器可以用三种不同的方法结束:
-
create()
将创建新的集合,如果集合已存在,则抛出异常 -
open()
打开已存在的集合,如果不存在则抛出异常 -
createOrOpen()
打开已存在的集合,否则创建新集合。
DB 不局限于集合,还能创建其他类型的记录,如原子记录:
Atomic.Var<Person> var = db.atomicVar("mainPerson",Person.SERIALIZER).createOrOpen();
Transactions事务
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.keySet() is now [1,2] even before commit
db.commit(); //persist changes into disk
map.put(3,"three");
//map.keySet() is now [1,2,3]
db.rollback(); //revert recent changes
//map.keySet() is now [1,2]
db.close();
其实和SQL的事务是差不多的。上面这个例子,key:1,2都已经提交,持久化操作到了磁盘中。而key:3并没有commit,而是回滚了。所有map.keySet() 现在还是只用两个key为 [1,2]。
我们将添加上官网例子中省略的DB的创建步骤,创建了一个MemoryDB。
public void test4() {
DB db = DBMaker
.memoryDB()
.make();
//#a
ConcurrentNavigableMap<Integer,String> map = db
.treeMap("collectionName", Serializer.INTEGER, Serializer.STRING)
.createOrOpen();
map.put(1,"one");
map.put(2,"two");
//map.keySet() is now [1,2] even before commit
db.commit(); //persist changes into disk
map.put(3,"three");
//map.keySet() is now [1,2,3]
db.rollback(); //revert recent changes
//map.keySet() is now [1,2]
db.close();
//#z
}
点击运行报错java.lang.UnsupportedOperationException: Store does not support rollback
不支持回滚操作。
我们能点击代码源码查看。
可以发现这是因为这个store !is StoreTx
导致的。
还记得我们之前在官网看到有官方的代码示例,我们前往进行查看GitHub - jankotek/mapdb-site: mapdb.org website
我们在src/test/java/doc/dbmaker_basic_tx.java
找到对应的示例。
发现它并没有什么不一样,复制运行还是报错。。。官方示例都不靠谱。。
我们在官网文档中搜索Transactions
一个个进去查看,终于在Performance
中找到。
我们使用DBMaker创建DB的时候要开启事务。transactionEnable()
public void test4() {
DB db = DBMaker
.memoryDB()
.transactionEnable()
.make();
//#a
ConcurrentNavigableMap<Integer, String> map = db
.treeMap("collectionName", Serializer.INTEGER, Serializer.STRING)
.createOrOpen();
map.put(1, "one");
map.put(2, "two");
//map.keySet() is now [1,2] even before commit
db.commit(); //persist changes into disk
map.put(3, "three");
//map.keySet() is now [1,2,3]
db.rollback(); //revert recent changes
//map.keySet() is now [1,2]
db.close();
//#z
}
运行成功~
总结
- 默认情况下,MapDB 使用通用序列化,可以序列化任何数据类型。使用专门的序列化程序会更快、更节省内存。此外,我们还可以在 64 位操作系统上启用更快的内存映射文件(只用用于数据存储在文件中fileDB,不能用于memoryDB)
- DBMaster 类处理数据库配置、创建和打开。 MapDB 有多种模式和配置选项。其中大部分可以使用此类进行设置。
- DB 实例代表已打开的数据库(或单个事务会话)。它可用于创建和打开集合存储。它还可以使用 commit()、rollback() 和 close() 等方法处理数据库的生命周期。
- 我们使用DBMaker创建DB的时候要开启事务
transactionEnable()
。
转载自:https://juejin.cn/post/7362757977735036979