良好的性能是一致性、速度和持久性之间妥协的结果。MapDB 提供了几个选项来实现这种妥协。有不同的存储实现、提交和磁盘同步策略、缓存、压缩………
本章概述了与性能和持久性相关的选项。有些选项会以牺牲速度为代价,确保存储写入的持久性。其他一些设置可能会导致内存泄漏、数据损坏,甚至JVM崩溃!请务必了解这些影响,并阅读 DBMaker 上的Javadoc文档。
如果存储未正确关闭,且未将待处理的更改刷新到磁盘,存储可能会损坏。这种情况通常发生在JVM进程崩溃或被强制终止时。
为防止文件损坏,MapDB 提供了预写日志(WAL)。这是一种可靠且简单的方法,可确保文件更改具有原子性和持久性。许多数据库(包括 Posgresql 和 MySQL)都在使用预写日志。不过,预写日志的速度较慢,因为数据需要在文件之间进行多次复制和同步。
WAL默认是禁用的。可以通过以下方式启用 DBMaker.transactionEnable():
DB db = DBMaker
.fileDB(file)
.transactionEnable()
.make();禁用预写日志(WAL)时(默认情况下禁用),您将没有崩溃保护。在这种情况下,您必须正确关闭存储,否则会丢失所有数据。MapDB 会检测到不正常关闭,并拒绝打开这种损坏的存储。有一种方法可以在只读模式下打开损坏的存储并进行数据恢复。
有一个关闭钩子,可在 JVM 退出前自动关闭数据库,但是如果 JVM 崩溃或被终止,它无法保护您的数据。请使用 DBMaker.closeOnJvmShutdown() 选项来启用它。
禁用事务后,您将没有回滚功能,db.rollback() 会抛出异常。db.commit() 将没有可提交的内容(所有数据都已存储),因此它会执行次优操作:提交会尝试刷新所有写入缓存并同步存储文件。因此,如果您调用 db.commit() 且不再进行任何写入操作,那么在 JVM 崩溃的情况下,您的存储应该是安全的(不会丢失数据)。
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 的知识,请继续学习后续教程🚀