掘金 后端 ( ) • 2024-04-29 09:52

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 来编写基于内存的处理程序。

Map - MapDB

image-20240413192733444.png

可以看到官网的介绍

MapDB提供了Java Maps,Sets,LIsts,Queues和其它堆外或磁盘存储支持的集合。它是java集合框架和嵌入式数据库引擎的混合体。它是在 Apache 许可下免费且开源的。

我们点击DOC,查看它的官方文档。

image-20240413193108743.png 文档提供了三种格式

  1. Git Books
  2. PDF
  3. ebook

并且所有文档的代码案例都上传到了Github仓库。Github repository.

我们点击查看Git Books的官方文档。

image-20240413193334073.png 这里又介绍了以下MapDB

MapDB 可能是速度最快的 Java 数据库,其性能可与 java.util 集合相媲美。它还提供 ACID 事务、快照、增量备份等高级功能。

快速开始

image-20240413193710683.png

MapDB 非常灵活,有很多配置选项。但在大多数情况下,只需几行代码就能完成配置。

MapDB也已经上传到了Maven中央仓库,通过xml文件就可以引用了。

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

Maven Repository: org.mapdb » mapdb (mvnrepository.com)

image-20240413193952421.png

我们可以看到目前MapDB的最新版本为3.1.0

Hello World

image-20240413194541913.png

这里给出了一个简单的示例,它打开内存中的 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>

image-20240413195029831.png

    public void test1() {
        DB db = DBMaker.memoryDB().make();
        ConcurrentMap map = db.hashMap("map").createOrOpen();
        map.put("something", "here");
        System.out.println(map.get("something"));
    }

image-20240413195437193.png

我们存储了一个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();
    }

image-20240413195845878.png

这时候我们运行程序,可以看到也成功拿到了对应的数据。并在程序目录下生成了一个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();
    }

image-20240413200442329.png

DB and DBMaker

可以从Hello World的例子中看到这两个类。那么这两个类分别是什么呢?我们一起来看官网学习吧。

image-20240413201144166.png

MapDB 像乐高积木一样是可插拔的。有两个类充当不同部分之间的粘合剂,即 DBMasterDB 类。

  1. DBMaster 类处理数据库配置、创建和打开。 MapDB 有多种模式和配置选项。其中大部分可以使用此类进行设置。
  2. DB 实例代表已打开的数据库(或单个事务会话)。它可用于创建和打开集合存储。它还可以使用 commit()rollback()close() 等方法处理数据库的生命周期。

要打开(或创建)一個储存库,使用 DB 静态方法之一,如 DBMaker.fileDB()。

MapDB 有更多格式和模式,因此每个 xxxDB() 使用不同的模式:

  • memoryDB():打开一个由字节[]数组支持的内存数据库
  • appendFileDB():打开一个只使用追加日志文件的数据库,等等。

我们可以查看源码,查看所支持的全部模式。

image-20240428181931287.png

在 xxxDB() 方法之后是一个或多个配置选项,最后是 make() 方法,该方法应用所有选项,打开选定的存储并返回 DB 对象。

官网提供了一个示例将打开一个已启用加密的文件存储:

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

Open and create collection

image-20240413201845741.png

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
    }

image-20240428174208538.png

点击运行报错java.lang.UnsupportedOperationException: Store does not support rollback 不支持回滚操作。

我们能点击代码源码查看。

image-20240428180736281.png

可以发现这是因为这个store !is StoreTx 导致的。

还记得我们之前在官网看到有官方的代码示例,我们前往进行查看GitHub - jankotek/mapdb-site: mapdb.org website

image-20240428180915705.png

我们在src/test/java/doc/dbmaker_basic_tx.java找到对应的示例。

image-20240428181013442.png

发现它并没有什么不一样,复制运行还是报错。。。官方示例都不靠谱。。

我们在官网文档中搜索Transactions

image-20240428181112213.png

一个个进去查看,终于在Performance中找到。

image-20240428181214432.png

我们使用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
    }

运行成功~

总结

  1. 默认情况下,MapDB 使用通用序列化,可以序列化任何数据类型。使用专门的序列化程序会更快、更节省内存。此外,我们还可以在 64 位操作系统上启用更快的内存映射文件(只用用于数据存储在文件中fileDB,不能用于memoryDB
  2. DBMaster 类处理数据库配置、创建和打开。 MapDB 有多种模式和配置选项。其中大部分可以使用此类进行设置。
  3. DB 实例代表已打开的数据库(或单个事务会话)。它可用于创建和打开集合存储。它还可以使用 commit()rollback()close() 等方法处理数据库的生命周期。
  4. 我们使用DBMaker创建DB的时候要开启事务transactionEnable()