RxDB Memory-Mapped RxStorage:内存读写与底层持久化相结合的混合存储加速方案
【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdb
Memory-Mapped RxStorage 是 RxDB Premium 提供的一个存储包装器(wrapper):它在内存中维护一份完整数据副本用于查询与写入,同时在后台把变更同步到任意底层持久化 RxStorage。本文从架构原理、优缺点、多标签页限制、加密组合、写入持久化保证、块大小配置到存储迁移,完整讲解这一"内存加速 + 磁盘持久"混合方案的配置方式与适用场景。
什么是 Memory-Mapped RxStorage
RxDB 本身不自带数据引擎,所有数据都存放在实现了 RxStorage 接口 的存储实现中,例如浏览器里的 LocalStorage、IndexedDB,Node.js 下的 SQLite、FoundationDB 等。这种可插拔设计让你可以根据运行环境与性能需求自由更换底层存储,详见 RxStorage 概览。
Memory-Mapped RxStorage(下文简称"内存映射存储")正是这一生态中的一种包装器型存储:它包装任意其他 RxStorage,在内存中创建一份用于查询与写入的存储实例,并让这份内存实例与给定的底层存储保持持久同步。其核心思路与普通的 Memory RxStorage 不同——后者只存在内存、进程退出即丢失;而内存映射存储把内存当作"读写的热层",把底层存储当作"落盘的冷层",从而同时获得内存级的速度与磁盘级的持久性。
从底层实现看,内存热层本质上复用了基于纯 JavaScript 数组与二分查找实现的 getRxStorageMemory,它没有磁盘 I/O、没有 JSON 序列化开销,这正是读写性能的来源;而落盘与同步逻辑则由getMemoryMappedRxStorage()(来自rxdb-premium/plugins/storage-memory-mapped)负责协调。
工作原理:内存热层 + 持久冷层
要理解内存映射存储的行为,需要把握三个关键机制:
- 初始化批量加载:创建数据库时,存储会把底层持久层中的全部数据通过一次批量请求读入内存。正因为是单次 bulk 读取,首次页面加载的数据读取开销可以被压缩到最小;若检测到数据库是全新创建(底层没有任何数据),甚至无需等待持久存储实例创建完成即可开始使用。
- 写入先内存、落盘异步:写操作默认直接作用于内存态并立刻返回,持久化在后台异步进行。
- "区块链式"块结构:为了兼顾快速首屏加载与低写入延迟,内存映射存储将数据以类似区块链的追加式块(block)结构保存——写入被追加进新的块而不是原地修改状态。这些块会在 CPU 空闲时被惰性清理与合并(参见下文"块大小限制"一节),底层对应 RxDB 的 requestIdlePromise(经由数据库的 idleQueue 实现,见 rx-database.ts)。
这种"写追加、空闲合并"的设计,把随机写转换为顺序追加写,显著降低了写入延迟,同时通过空闲期的后台整理保证后续读取与初始加载的高效。
优点(Pros)
- 读写性能提升:查询与写入直接运行在内存存储上,绕过了磁盘 I/O 与序列化开销。
- 首屏加载更快:全部数据在一次 bulk 请求中载入;首次使用时甚至无需等待持久存储的创建。
- 加密数据可查询:可以把文档数据加密后落盘,同时仍然能够在未加密的内存态上运行查询(详见下文"持久化数据的加密"一节)。
缺点与限制(Cons)
- 不支持附件(attachments):因为大体积附件数据不应保存在内存中,见 rx-attachment.md。
- 异常终止可能丢失写入:当 JavaScript 进程被非正常终止(如浏览器崩溃、电脑断电)时,部分尚未同步到父存储的内存写入可能丢失。可通过
awaitWritePersistence标志规避。 - 数据必须能全部装入内存:内存映射存储要求全部数据放入 JavaScript 进程的内存。通常情况下这不是问题——现代浏览器内存充足,纯 JSON 文档数据量并不大。
- 首次加载可能变慢:因为需要把底层已有数据一次性读入内存,当已存数据很多时,初始页面加载时间可能增加。存储文档数量小于约
10k时通常没有影响。
基本用法:包装任意持久化存储
内存映射存储的接入非常简单:先用getRxStorageIndexedDB()(或其他任意 RxStorage)创建持久层,再用getMemoryMappedRxStorage({ storage: parentStorage })包装它,最后把这个包装后的 storage 传入createRxDatabase即可。其余 RxDB API 完全不受影响。
import { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb'; import { getMemoryMappedRxStorage } from 'rxdb-premium/plugins/storage-memory-mapped'; /** * 这里使用 IndexedDB RxStorage 作为持久化存储, * 任何其他 RxStorage 也都可以使用。 */ const parentStorage = getRxStorageIndexedDB(); // 用内存映射存储包装持久化存储 const storage = getMemoryMappedRxStorage({ storage: parentStorage }); // 像使用任何其他 RxStorage 一样创建 RxDatabase const db = await createRxDatabase({ name: 'myDatabase', storage, }); /** ... **/典型组合:浏览器低延迟场景
在 RxStorage 配置示例 中,官方给出了一个面向"低写入延迟 + 简单读取"的浏览器配置:用内存映射存储做热层,用主线程的 OPFS 存储做持久层,再叠加 LocalStorage Meta Optimizer 优化初始化元数据读取。之所以不用 Worker,是因为主线程与 Worker 之间来回传输数据本身会增加延迟:
import { getLocalstorageMetaOptimizerRxStorage } from 'rxdb-premium/plugins/storage-localstorage-meta-optimizer'; import { getMemoryMappedRxStorage } from 'rxdb-premium/plugins/storage-memory-mapped'; import { getRxStorageOPFSMainThread } from 'rxdb-premium/plugins/storage-worker'; const myDatabase = await createRxDatabase({ storage: getLocalstorageMetaOptimizerRxStorage({ storage: getMemoryMappedRxStorage({ storage: getRxStorageOPFSMainThread() }) }) });典型组合:Node.js 服务端场景
在 Node.js 数据库指南 中,官方展示了如何在 Node.js 下用 FoundationDB 作为持久层实现"内存数据库的性能 + 数据的持久化":
import { createRxDatabase } from 'rxdb'; import { getRxStorageFoundationDB } from 'rxdb/plugins/storage-foundationdb'; import { getMemoryMappedRxStorage } from 'rxdb-premium/plugins/storage-memory-mapped'; const db = await createRxDatabase({ name: 'exampledb', storage: getMemoryMappedRxStorage({ storage: getRxStorageFoundationDB({ apiVersion: 620, clusterFile: '/path/to/fdb.cluster' }) }) });需要注意,这种"内存 + 持久"方案有两个固有代价:数据库大小受限于内存容量;Node.js 进程若在"写入内存态"与"后台持久化"之间退出,写入可能丢失。针对后者,请在服务端场景设置awaitWritePersistence: true。
在 RxDB Server 扩容指南 中也有类似用法:面对大量用户请求时,把内存映射存储放在用户侧,用文件系统 Node 存储做持久层,从而让服务器端读取直接命中内存层;文中同样提醒,若担心服务器崩溃导致内存层写入丢失,应在内存映射存储配置中打开awaitWritePersistence。
多标签页支持(Multi-Tab)
由于内存映射存储的工作方式所限,同一个存储无法在多个 JavaScript 进程中同时打开。因此在浏览器应用里,当应用被多个浏览器标签页使用时,你不能在多个标签页中各自打开数据库。
解决方案是配合 SharedWorker Plugin:让内存映射存储运行在 SharedWorker 中,且只初始化一次,随后被所有浏览器标签页复用。
如果你运行在单一 JavaScript 进程中(例如 React Native 应用),则无需关心这一点,直接在主进程中使用内存映射存储即可。
持久化数据的加密
常规情况下,RxDB 无法在加密字段上运行查询。但使用内存映射存储后,你可以把文档数据加密后写入磁盘,同时仍然能在未加密的内存态上执行查询。
关键点在于:加密存储包装器要包在持久化存储外面,而不是包在内存映射存储整体外面。即先wrappedKeyEncryptionWebCryptoStorage({ storage: getRxStorageIndexedDB() })得到加密持久层,再把它作为storage传入getMemoryMappedRxStorage:
import { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb'; import { getMemoryMappedRxStorage } from 'rxdb-premium/plugins/storage-memory-mapped'; import { wrappedKeyEncryptionWebCryptoStorage } from 'rxdb-premium/plugins/encryption-web-crypto'; const storage = getMemoryMappedRxStorage({ storage: wrappedKeyEncryptionWebCryptoStorage({ storage: getRxStorageIndexedDB() }) }); const db = await createRxDatabase({ name: 'myDatabase', storage, }); /** ... **/这样,落盘数据是密文、内存态是明文,查询在明文上进行,兼顾了"数据静态加密"与"字段可查询"。加密的完整配置与密码管理方式见 encryption.md。
Await Write Persistence:等待落盘完成
默认情况下,内存映射存储上的操作在内存态执行完就立即返回,变更在后台异步持久化。若你希望确保某个写操作确实已持久化到底层存储,可以把awaitWritePersistence设为true:
const storage = getMemoryMappedRxStorage({ awaitWritePersistence: true, storage: getRxStorageIndexedDB() });代价是每次写入都要等待持久层完成,写入延迟会相应上升。因此它适合"宁可慢一点,也不能丢数据"的关键写入场景;而默认的异步模式适合对吞吐与延迟更敏感、可容忍极端情况下少量写入丢失的场景。
Block Size Limit:控制合并块大小
在清理(cleanup)过程中,内存映射存储会把许多小的写入块合并成少数大块,以换取更好的初始加载性能。blockSizeLimit定义了单个块中最多能存放多少文档,默认值为10000:
const storage = getMemoryMappedRxStorage({ blockSizeLimit: 1000, storage: getRxStorageIndexedDB() });调小该值会产生更多、更小的块(清理更频繁),调大则产生更少、更大的块(首屏加载更快但单次合并开销更大)。当你的单集合文档数接近或超过默认值时,建议结合数据集规模评估是否需要调整。
从其他存储迁移到内存映射存储
当你从"普通"持久化存储(如 IndexedDB 或 SQLite)切换到内存映射存储时,必须使用 Storage Migrator(存储迁移插件) 迁移数据,不能直接在一个已存在的数据库上替换存储适配器——因为内存映射存储使用完全不同的内部数据结构(区块链式块结构)。
存储迁移的基本用法(以从 LocalStorage 迁移到 IndexedDB 为例,同样的流程适用于迁入内存映射存储):
import { migrateStorage } from 'rxdb/plugins/migration-storage'; import { getRxStorageIndexedDB } from 'rxdb-premium/plugins/storage-indexeddb'; import { getRxStorageLocalstorage } from 'rxdb-old/plugins/storage-localstorage'; // 创建新的 RxDatabase(新库名必须与旧库不同) const db = await createRxDatabase({ name: dbLocation, storage: getRxStorageIndexedDB(), multiInstance: false }); await migrateStorage({ database: db as any, oldDatabaseName: 'myOldDatabaseName', // 旧数据库名 oldStorage: getRxStorageLocalstorage(), // 旧数据库使用的 RxStorage batchSize: 500, // 批大小 parallel: false, // true: 并行迁移所有集合;false(默认): 串行迁移 afterMigrateBatch: (input: AfterMigrateBatchHandlerInput) => { console.log('storage migration: batch processed'); } });需要特别注意的几点:
- 迁移期间不要同时改 schema:若还想改 schema,应先完成存储迁移,再执行常规的 schema 迁移,否则可能导致数据库不可用。
- 只迁移已定义的集合:调用
migrateStorage()时新数据库中不存在的集合不会被迁移(会被跳过),你可以借此只迁移部分集合。 - 删除的文档会被丢弃:存储迁移会过滤并丢弃已删除文档。
决策建议:何时使用内存映射存储
综合本文的分析,可以给出如下选型判断:
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 需要查询/写入性能又要求数据落盘 | ✅ 推荐 | 内存热层 + 磁盘冷层的组合,性能与持久性兼得 |
| 数据量可完整装入内存(约 < 10k 文档) | ✅ 推荐 | 初始加载影响可忽略,收益最大 |
| 需要加密落盘且字段可查询 | ✅ 推荐 | 加密持久层 + 明文内存态是独有能力 |
| 浏览器多标签页同时使用 | ⚠️ 需配合 SharedWorker | 同一存储不可被多进程打开 |
| 写入后进程随时可能被杀 | ⚠️ 需开启awaitWritePersistence | 否则极端情况下可能丢写 |
| 需要存储附件(attachments) | ❌ 不推荐 | 内存映射存储不支持附件 |
| 数据集远超内存容量 | ❌ 不推荐 | 数据必须整体装入内存 |
简言之,内存映射存储最适合"数据量可控、读多写多、需要持久化"的本地优先(local-first)应用;接入前务必评估数据规模、多标签页需求与写入可靠性要求,必要时配合 SharedWorker、awaitWritePersistence与存储迁移插件一起使用。
【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考