MapDB 的性能与持久性

良好的性能是一致性、速度和持久性之间妥协的结果。MapDB 提供了几个选项来实现这种妥协。有不同的存储实现、提交和磁盘同步策略、缓存、压缩………

本章概述了与性能和持久性相关的选项。有些选项会以牺牲速度为代价,确保存储写入的持久性。其他一些设置可能会导致内存泄漏、数据损坏,甚至JVM崩溃!请务必了解这些影响,并阅读 DBMaker 上的Javadoc文档

事务和崩溃保护

如果存储未正确关闭,且未将待处理的更改刷新到磁盘,存储可能会损坏。这种情况通常发生在JVM进程崩溃或被强制终止时。

为防止文件损坏,MapDB 提供了预写日志(WAL)。这是一种可靠且简单的方法,可确保文件更改具有原子性和持久性。许多数据库(包括 PosgresqlMySQL)都在使用预写日志。不过,预写日志的速度较慢,因为数据需要在文件之间进行多次复制和同步。

WAL默认是禁用的。可以通过以下方式启用 DBMaker.transactionEnable()

DB db = DBMaker
        .fileDB(file)
        .transactionEnable()
        .make();

禁用预写日志(WAL)时(默认情况下禁用),您将没有崩溃保护。在这种情况下,您必须正确关闭存储,否则会丢失所有数据。MapDB 会检测到不正常关闭,并拒绝打开这种损坏的存储。有一种方法可以在只读模式下打开损坏的存储并进行数据恢复。

有一个关闭钩子,可在 JVM 退出前自动关闭数据库,但是如果 JVM 崩溃或被终止,它无法保护您的数据。请使用 DBMaker.closeOnJvmShutdown() 选项来启用它。

禁用事务后,您将没有回滚功能,db.rollback() 会抛出异常。db.commit() 将没有可提交的内容(所有数据都已存储),因此它会执行次优操作:提交会尝试刷新所有写入缓存并同步存储文件。因此,如果您调用 db.commit() 且不再进行任何写入操作,那么在 JVM 崩溃的情况下,您的存储应该是安全的(不会丢失数据)。

内存映射文件(mmap)

MapDB 从设计之初就充分利用了内存映射文件(mmap)。然而,在 32 位 JVM 上,由于地址空间限制,内存映射文件(mmap)的大小被限制在 4GB。当 JVM 耗尽地址空间时,会产生不良影响,例如 JVM 崩溃。默认情况下,我们使用一种速度较慢但更安全的磁盘访问模式,称为随机访问文件(RAF)。

与 RAF 相比,内存映射文件(Mmap files)的速度要快得多。确切的速度提升幅度取决于操作系统和磁盘情况管理,但通常在 10% 到 300% 之间。

内存映射文件可通过 DBMaker.fileMmapEnable() 设置来激活。

也可以仅在检测到 64 位平台时激活内存映射文件:DBMaker.fileMmapEnableIfSupported()

内存映射文件(Mmap files)高度依赖于操作系统。例如,在 Windows 系统上,如果内存映射文件被 JVM 锁定,你就无法删除它。如果 Windows 系统上的 JVM 在未关闭内存映射文件的情况下崩溃,你必须重启 Windows 才能释放文件锁。

JVM 中也存在一个程序缺陷。内存映射文件句柄要等到 DirectByteBuffer 被垃圾回收后才会释放。这意味着即使调用了 db.close(),内存映射文件仍然会保持打开状态。在 Windows 系统上,这会导致文件无法重新打开或删除。在 Linux 系统上,这会消耗文件描述符,一旦所有描述符都被用完,就可能会引发错误。

有一个使用未公开 API 来解决此程序缺陷的办法。但在极少数情况下,它会导致 JVM 崩溃,因此默认处于禁用状态。可以使用 DBMaker.cleanerHackEnable() 来启用它。

以下是所有与内存映射(mmap)相关的选项示例:

DB db = DBMaker
    .fileDB(file)
    .fileMmapEnable()            // 始终启用内存映射(mmap)
    .fileMmapEnableIfSupported() // 仅在受支持的平台上启用内存映射(mmap)
    .fileMmapPreclearDisable()   // 加快内存映射文件的速度

    // 关闭文件时取消映射(释放资源)。
    // 如果文件在被取消映射后又被访问,可能会导致JVM崩溃
    // (可能存在竞争条件)。
    .cleanerHackEnable()
    .make();

// 可选地将文件内容预加载到磁盘缓存中
db.getStore().fileLoad();

文件通道

默认情况下,MapDB 使用 RandomAccessFile(随机访问文件)来访问磁盘存储。除了快速的内存映射(mmap)文件之外,还有第三种基于 FileChannel(文件通道)的选项。它应该比 RandomAccessFile(随机访问文件)更快,但会有稍多的开销。它在并发访问下的表现也更好(随机访问文件有全局锁)。

FileChannel 与 Thread.interrupt 结合使用时会产生问题。如果线程在执行 IO 操作时被中断,底层通道会对所有其他线程关闭。

要使用 FileChannel,请使用 DBMaker.fileChannelEnable() 选项:

DB db = DBMaker
    .fileDB(file)
    .fileChannelEnable()
    .make();

内存模式

MapDB 有三种内存存储:

(1)堆内存储:它将对象存储在 Map<recid,Object> 中,且不使用序列化。这种模式对于小型数据集来说速度非常快,但会受到垃圾回收(GC)的影响,因此当数据达到几 GB 后,性能会断崖式下降。它可以通过以下方式激活:

DB db = DBMaker
    .heapDB()
    .make();

(2)基于 byte[] 的存储:在这种模式下,数据会被序列化并存储到 1MB 大小的 byte[] 中。从技术上讲,这仍然是堆内存储,但不受垃圾回收(GC)开销的影响,因为数据对 GC 是不可见的。默认情况下推荐使用这种模式,因为它不需要任何额外的 JVM 设置。通过 -Xmx10G 这个 JVM 参数来增加最大堆内存就足够了。

DB db = DBMaker
    .memoryDB()
    .make();

(3)基于 DirectByteBuffer 的存储:在这种情况下,数据完全存储在堆外,存储在通过 ByteBuffer.allocateDirect(size) 创建的 1MB 大小的 DirectByteBuffer 中。你需要通过 JVM 参数增加最大直接内存。这种模式允许你将最大堆大小减小到非常小的程度(-Xmx128M)。较小的堆大小通常具有更好且更可预测的性能。

// 运行命令:java -XX:MaxDirectMemorySize=10G
DB db = DBMaker
    .memoryDirectDB()
    .make();

分配选项

默认情况下,MapDB 会尽量减少空间使用,并以 1MB 的增量分配空间。这种额外的分配可能比单次大尺寸分配更慢。有两个选项可以控制存储的初始大小和大小增量。此示例将初始分配 10GB,然后以 512MB 的块为单位增加大小:

DB db = DBMaker
    .fileDB(file)
    .fileMmapEnable()
    .allocateStartSize(10 * 1024*1024*1024)   // 10GB
    .allocateIncrement(512 * 1024*1024)       // 512MB
    .make();

分配增量对内存映射文件(mmap 文件)的性能有副作用。

MapDB 通过一系列直接字节缓冲区(DirectByteBuffer)来映射文件。每个缓冲区的大小等于分配增量(默认是 1MB),因此,对于相同的磁盘存储大小,分配增量越大,所需的缓冲区就越少。像同步(sync)、刷新(flush)和关闭(close)这类操作需要遍历所有缓冲区。所以,更大的分配增量可能会加快提交(commit)和关闭(close)操作的速度。

更多关于 MapDB 的知识,请继续学习后续教程🚀

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号