RxDB 作为 RethinkDB 替代方案:离线优先的客户端响应式数据库实战指南
2026/9/20 11:57:37 网站建设 项目流程
  • 数据库
  • NoSQL
  • 嵌入式数据库
  • 实时数据库

【免费下载链接】rxdb

The 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
点击查看免费下载

RethinkDB 曾以 changefeed(变更推送)机制引领了实时数据库的风潮,但作为服务端数据库,它天然无法满足现代离线优先(offline-first)应用的需求。本文以 RxDB 仓库中的对比文档为主体,系统剖析 RethinkDB 的架构短板,并逐一展示 RxDB 如何在客户端实现等效的响应式编程模型——包括响应式查询、离线写入、多后端复制、多标签页同步与冲突解决。读完本文,你将掌握用 RxDB 构建"断网也能完整工作、联网后自动同步"的实时应用的完整方案。


什么是 RethinkDB

RethinkDB 是 2009 年创立、2012 年公开推出的分布式文档型数据库,定位是实时应用。它的标志性能力是changefeed:客户端与数据库服务器之间建立持久连接,插入、更新、删除等变更事件一旦发生就被实时推送给客户端,无需轮询。

RethinkDB 使用自研的ReQL查询语言,它可链式调用并直接嵌入宿主语言(JavaScript、Python、Ruby)。一个带 changefeed 的基础查询如下:

// RethinkDB: subscribe to all changes in the 'messages' table r.table('messages') .changes() .run(connection, (err, cursor) => { cursor.each((err, change) => { console.log('Old value:', change.old_val); console.log('New value:', change.new_val); }); });

changefeed 的思路在当时确实新颖。在此之前,构建实时应用通常需要轮询、在普通数据库之上搭建 WebSocket 基础设施,或者在存储之上再叠加一层专用的 pub/sub 系统。RethinkDB 把它直接做进了查询层。

RethinkDB 时间线

  • 2009—— RethinkDB Inc. 成立,项目最初是面向 SSD 优化的存储引擎。
  • 2012—— RethinkDB 1.0 公开亮相,成为带 ReQL 与 changefeed 的实时文档数据库。
  • 2013–2015—— 活跃开发期,社区持续壮大,实时应用团队兴趣浓厚。
  • 2016 年 10 月—— RethinkDB Inc. 关停。创始团队发布事后总结(post-mortem),承认在与 MongoDB 及云托管数据库的竞争中未能建立可持续的商业模式。
  • 2017 年 2 月—— Linux 基金会(经由 CNCF)接管 RethinkDB,并以 Apache License 2.0 重新授权,社区继续维护。
  • 2018 年至今—— 社区贡献者仅提供零星的 bug 修复与维护性版本,几乎没有新的功能开发。项目功能稳定但不再积极演进。

创始团队的事后总结相当坦诚:数据库市场奖励的是运维简单与托管服务,相比 Firebase 或后来的 Supabase 等托管方案,RethinkDB 运维门槛过高。已在用 MongoDB 的团队,也几乎没有理由仅为了 changefeed 而迁移——何况 MongoDB 后来也加入了自己的 change streams 功能。

RethinkDB 的强项

在联网环境下做"服务器到客户端的实时数据流",RethinkDB 的架构是干净利落的:ReQL 表达力强,changefeed 深度融入查询模型,分布式架构支持跨节点的分片与复制。对监控传感器数据的仪表盘、假设所有用户都在线的聊天应用这类场景,RethinkDB 确实优雅地解决了一个真实问题。


RethinkDB 的短板

没有离线能力

RethinkDB 是服务端数据库,应用通过向 RethinkDB 集群发送网络请求来查询数据。用户一旦断网,所有读写立即失败。

这并非配置问题或缺少插件,而是架构上就没有客户端存储:没有可离线响应查询的本地缓存;网络一断,所有 changefeed 全部断开,驱动直接报错;客户端离线期间发生的变更,RethinkDB 也不会为每个客户端缓冲错过的历史事件。

配套的客户端库 Horizon(提供认证与订阅辅助能力)自始至终没有实现离线支持,2016 年提出的相关 issue 在公司关停时未获解决即被关闭。对现代 Web 与移动应用而言,离线绝非边缘场景——用户在火车上、信号差的建筑里、网络时断时续的环境中打开应用是常态,断网即报错的应用体验是难以接受的。

changefeed 无法跨断线存活

当 changefeed 客户端断开再重连时,它不会自动补收离线期间发生的变更。应用必须重新建立连接、重跑查询,并自行在"最后已知状态"与"当前服务端状态"之间做对账。

服务器在内存中最多缓冲changefeed_queue_size(默认 100,000 条事件)的变更。如果客户端离线时间过长导致缓冲写满,服务器会丢弃事件并向客户端返回错误。此时应用拿到的是一份不完整的状态视图,只能全量重读。这套架构把大量复杂度推给了应用代码——每个用到 changefeed 的功能都要自行处理重连、回填与缓冲溢出。

服务端架构意味着基础设施运维

在生产环境运行 RethinkDB,意味着要管理一个集群:分片、复制因子、服务器拓扑都要手工配置。相比 Firebase、Supabase 这类云托管数据库,RethinkDB 把运维责任全部压给了使用团队。这也是创始团队事后总结中提到的败因之一:开发者更偏好托管服务,因为运维复杂度被抽象掉了。公司关停后,RethinkDB 也没有官方支持合同或托管服务。

社区维护而非积极开发

RethinkDB 由志愿者维护,只收 bug 修复、不收新功能。对 Deno、Bun 等新 JavaScript 运行时以及现代生态工具链的驱动支持,远不如仍在积极开发的数据库。对 2025/2026 年启动的新项目而言,押注一个没有商业背书、没有托管服务、没有功能路线图的数据库是有风险的:一旦发现安全漏洞或与新版 Node.js 不兼容,修复只能指望没有回应义务的社区志愿者。

ReQL 不可移植

ReQL 是 RethinkDB 专属的。学到的 ReQL 知识无法迁移到其他数据库,切换存储后端时 ReQL 查询也无法复用。相比之下,MongoDB 风格的查询语法(RxDB 等也实现了该语法)拥有大得多的社区知识库。

生态数据

数据也印证了这一点:截至 2026 年 7 月 30 日,rethinkdb包在 npm 上最近 30 天的下载量为 65,703 次,而rxdb为 270,494 次。


RxDB 如何应对同样的问题

RxDB 是面向客户端环境的本地优先(local-first)JavaScript 数据库,覆盖浏览器、React Native、Electron 与 Node.js。它与 RethinkDB 有着相同的响应式目标(数据变更应自动传导到 UI),但实现方式是把响应式放到客户端,而不是依赖一条常驻的服务器连接。

无需服务器连接的响应式查询

在 RxDB 中,每一个查询都是可观察的(observable)。订阅查询结果时,你会立刻收到当前结果集;此后无论底层数据发生何种变化——来自本地写入还是复制事件——observable 都会重新发射最新结果:

import { createRxDatabase } from 'rxdb/plugins/core'; import { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb'; const db = await createRxDatabase({ name: 'myapp', storage: getRxStorageIndexedDB() }); await db.addCollections({ messages: { schema: { title: 'message schema', version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 100 }, text: { type: 'string' }, roomId: { type: 'string' }, createdAt: { type: 'number' } }, required: ['id', 'text', 'roomId', 'createdAt'], indexes: ['roomId', 'createdAt'] } } }); // Subscribe to all messages in a specific room, sorted by creation time db.messages.find({ selector: { roomId: 'room-42' }, sort: [{ createdAt: 'asc' }] }).$.subscribe(messages => { renderChatUI(messages); // called immediately and on every change });

这段代码完全离线运行。查询针对的是 IndexedDB(或任意配置的存储),而不是远程服务器,没有需要建立的连接,也没有需要处理的断线。

RxDB 使用 event-reduce 算法让响应式更新保持高效:一次文档写入发生后,RxDB 会判断能否直接把变更应用到现有查询结果上,从而跳过整条查询的重新执行。从源码看,该模块对每个变更事件都会给出一个明确结论——runFullQueryAgain: false时直接产出newResults(增量更新),仅在无法安全增量时才退化为runFullQueryAgain: true的全量重查。这使得写密集场景下的响应式 UI 更新依然很快。

真正的离线优先

用户在没有网络的情况下打开 RxDB 应用,所有功能照常工作:写入落到本地存储,查询从本地存储返回,UI 渲染时没有任何加载转圈或报错状态。

网络恢复后,RxDB 的复制插件会在后台把本地变更同步到远端后端;用户再次离线,本地数据库继续工作。这正是 offline-first 架构 所描述的模式——本地数据库而非服务器,成为应用所有持久状态变更的入口。

RethinkDB 的架构做不到这一点:数据在服务器上,离线等于没有数据。想获得真正的离线优先,必须额外叠加一层本地存储并自己编写对账逻辑——到那时,RethinkDB 只是后端,而不是实时客户端数据库。

灵活的存储后端

RxDB 拥有可插拔的存储层。同一份应用代码,可依据运行环境切换到不同的存储引擎:

环境存储方案
浏览器(标准)IndexedDB
浏览器(高吞吐)OPFS(Origin Private File System)
React Native / ExpoSQLite(expo-sqlite 或 op-sqlite)
Node.js / ElectronSQLite(better-sqlite3)
多标签页浏览器SharedWorker
测试 / CIMemory

切换存储只需在创建数据库时改一个参数:

import { getRxStorageOpfs } from 'rxdb/plugins/storage-opfs'; const db = await createRxDatabase({ name: 'myapp', storage: getRxStorageOpfs() // use OPFS for better browser performance });

可复制到任意后端

RethinkDB 同时充当存储层与实时传输层,RxDB 则将两者解耦:数据存在本地,复制到后端是独立、可配置的插件。

HTTP 复制插件 对接任意 REST/HTTP 端点;GraphQL 复制插件 可连接 GraphQL API(包括 AWS AppSync);WebSocket 复制插件 提供来自服务器的低延迟推送;CouchDB 复制插件 走 CouchDB 的多主协议;你也可以为任意私有 API 实现自定义复制处理器。

import { replicateRxCollection } from 'rxdb/plugins/replication'; const replicationState = await replicateRxCollection({ collection: db.messages, replicationIdentifier: 'messages-http-v1', pull: { handler: async (checkpoint, batchSize) => { const url = `/api/messages/changes?since=${checkpoint?.updatedAt ?? 0}` + `&limit=${batchSize}`; const response = await fetch(url); const data = await response.json(); return { documents: data.documents, checkpoint: data.checkpoint }; } }, push: { handler: async (rows) => { const response = await fetch('/api/messages/push', { method: 'POST', body: JSON.stringify(rows), headers: { 'Content-Type': 'application/json' } }); return response.json(); // returns conflicting docs or [] } }, live: true, retryTime: 5000 }); // Observable replication state replicationState.active$.subscribe(active => console.log('Syncing:', active)); replicationState.error$.subscribe(err => console.error('Sync error:', err));

复制状态完全可观察。从复制插件的源码可以看到,RxReplicationState内部维护了receivedsenterrorcanceledactiveconflict共六个 Subject,并对外暴露received$sent$error$canceled$active$conflict$等 observable。你随时可以精确得知复制何时处于激活状态、何时出错、发了哪些文档、收了哪些文档,没有任何隐藏行为。

浏览器多标签页支持

RethinkDB 是服务器进程,没有"浏览器标签页"的概念。而在客户端,多个标签页各自持有独立内存状态,是常见的一致性问题的根源。

RxDB 用 SharedWorker 存储 解决这个问题:所有标签页共享一个运行在 SharedWorker 中的数据库实例,任意标签页的写入都会立即反映到其他标签页的响应式查询中:

import { getRxStorageSharedWorker } from 'rxdb/plugins/storage-shared-worker'; const db = await createRxDatabase({ name: 'myapp', storage: getRxStorageSharedWorker({ workerInput: new SharedWorker( new URL('rxdb/plugins/storage-shared-worker/worker.js', import.meta.url), { type: 'module' } ) }) });

对于"只需恰好一个标签页执行后台任务(比如跑复制)"的协调场景,RxDB 还提供领导者选举插件:一个标签页被选举为 leader 并执行后台任务,其余标签页等待;leader 标签页关闭后,另一个会自动接管。该能力对应源码中的waitForLeadership()方法(见 src/plugins/leader-election/index.ts):

import { RxDBLeaderElectionPlugin } from 'rxdb/plugins/leader-election'; import { addRxPlugin } from 'rxdb/plugins/core'; addRxPlugin(RxDBLeaderElectionPlugin); // Wait until this tab is the leader before starting replication await db.waitForLeadership(); startReplication(db);

可观察的变更事件

RxDB 在数据库和集合两个层级都暴露了 changestream(见 rx-database.md)。你可以订阅所有文档变更,这与 RethinkDB 的表级 changefeed 类似,但事件来自本地数据库而非服务器:

// Subscribe to all changes in the messages collection db.messages.$.subscribe(changeEvent => { console.log('Operation:', changeEvent.operation); // INSERT, UPDATE, DELETE console.log('Document ID:', changeEvent.documentId); console.log('Document data:', changeEvent.documentData); }); // Subscribe to changes on a specific document const doc = await db.messages.findOne('message-001').exec(); doc.$.subscribe(updatedDoc => { console.log('Document updated:', updatedDoc?.text); });

这是 RethinkDB 点级 changefeed 的客户端等价物,区别在于这些事件源于本地,因此用户离线时依然会触发。

冲突解决

在实时多用户系统中,两个用户可能同时编辑同一文档。RethinkDB 的冲突模型依赖服务器持有唯一权威视图——因为每次写入都立即经过服务器,这种模型才能成立。

RxDB 则是本地优先模型:用户可以在离线时本地编辑,联网后这些修改再同步。如果两个客户端在断连期间编辑了同一文档,同步时两个版本必须被调和。

RxDB 通过可配置的冲突处理器来处理:

await db.addCollections({ messages: { schema: messageSchema, conflictHandler: async ({ newDocumentState, realMasterState }) => { // Keep whichever version was updated more recently if (newDocumentState.updatedAt >= realMasterState.updatedAt) { return { documentData: newDocumentState }; } return { documentData: realMasterState }; } } });

对于合并语义至关重要的协作编辑场景(比如两个用户在文本的不同位置做了编辑),RxDB 支持基于 CRDT 的冲突解决。CRDT 无需中心权威即可确定性地合并并发编辑。插件通过getCRDTSchemaPart()为 schema 注入 CRDT 字段(见 src/plugins/crdt/index.ts),并在每次写入时把操作追加到crdtDocField.operations并重算哈希:

import { getCRDTSchemaPart, RxDBcrdtPlugin } from 'rxdb/plugins/crdt'; import { addRxPlugin } from 'rxdb/plugins/core'; addRxPlugin(RxDBcrdtPlugin); const messageSchema = { version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 100 }, text: { type: 'string' }, roomId: { type: 'string' }, crdts: getCRDTSchemaPart() }, crdt: { field: 'crdts' } };

Schema 校验与 TypeScript 支持

RxDB 会在写入存储之前,用 JSON Schema 校验每个文档。不符合 schema 的文档在数据库层就被拒绝,防止脏数据进入本地存储:

try { await db.messages.insert({ id: 'msg-001', // 'text' field is required but missing roomId: 'room-42', createdAt: Date.now() }); } catch (err) { console.error(err); // Schema validation error: missing 'text' }

RxDB 还会根据 schema 自动生成 TypeScript 类型,为所有集合操作提供 IDE 自动补全与编译期类型安全。

Schema 迁移

应用演进时数据模型会变化。RxDB 内置了 schema 迁移系统:当本地数据库以高于存量数据的 schema 版本打开时,RxDB 自动执行迁移。该机制在源码中对应migrationStrategies的注册与按版本顺序执行(见 src/plugins/migration-schema/index.ts 及 migration-helpers.ts):

await db.addCollections({ messages: { schema: messageSchemaV2, // version: 1 migrationStrategies: { 1: (oldDoc) => { // Migrate from version 0: add a 'roomId' field with a default return { ...oldDoc, roomId: oldDoc.roomId ?? 'general' }; } } } });

迁移在每台客户端的本地数据上独立运行,不需要协调的后端发布。

静态加密

RxDB 内置了加密插件,在写入本地存储之前对单个文档字段加密,适合在本地存储敏感用户数据的应用。wrappedKeyEncryptionCryptoJsStorage用密码包装存储密钥,并强制密码最短长度(MINIMUM_PASSWORD_LENGTH为 8,见 src/plugins/encryption-crypto-js/index.ts):

import { wrappedKeyEncryptionCryptoJsStorage } from 'rxdb/plugins/encryption-crypto-js'; import { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb'; const db = await createRxDatabase({ name: 'myapp', storage: wrappedKeyEncryptionCryptoJsStorage({ storage: getRxStorageIndexedDB() }), password: 'your-encryption-passphrase' }); const schema = { version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 100 }, text: { type: 'string' }, private: { type: 'string' } }, encrypted: ['private'] // stored as ciphertext in IndexedDB };

快速上手 RxDB

安装 RxDB 与 RxJS:

npm install rxdb rxjs

创建数据库、插入文档并订阅响应式查询:

import { createRxDatabase, addRxPlugin } from 'rxdb/plugins/core'; import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode'; import { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb'; addRxPlugin(RxDBDevModePlugin); const db = await createRxDatabase({ name: 'chatapp', storage: getRxStorageIndexedDB() }); await db.addCollections({ messages: { schema: { title: 'message schema', version: 0, primaryKey: 'id', type: 'object', properties: { id: { type: 'string', maxLength: 100 }, text: { type: 'string' }, roomId: { type: 'string' }, createdAt: { type: 'number' } }, required: ['id', 'text', 'roomId', 'createdAt'], indexes: ['roomId', 'createdAt'] } } }); // Write a message await db.messages.insert({ id: 'msg-001', text: 'Hello from RxDB!', roomId: 'room-42', createdAt: Date.now() }); // Reactive query: always reflects the current state db.messages.find({ selector: { roomId: 'room-42' }, sort: [{ createdAt: 'asc' }] }).$.subscribe(messages => { console.log('Current messages:', messages.map(m => m.text)); });

以上全部离线可用。需要服务器同步时,接上一个复制插件即可。


对比总结

维度RethinkDBRxDB
运行位置服务端集群客户端(浏览器、移动端、桌面端)
离线支持无(必须联网)完整的离线优先
响应式查询服务器推送 changefeed 事件客户端 observable 查询(基于 RxJS)
数据存放位置仅远端服务器本地存储(IndexedDB、OPFS、SQLite)
断线处理changefeed 断开,错过事件丢失本地数据库继续工作
查询语言ReQL(RethinkDB 专属)Mango(MongoDB 兼容 JSON)
后端依赖必须运行 RethinkDB 集群任意后端或无需后端
冲突解决服务器权威(最后写入获胜)可配置的客户端处理器或 CRDT
多标签页支持不适用(服务器概念)SharedWorker(跨标签页共享状态)
Schema 校验每次写入强制执行 JSON Schema
Schema 迁移手动内置版本化迁移策略
静态加密无内置内置字段级加密插件
TypeScript社区维护的类型声明从 schema 自动生成
当前状态2017 年起社区维护2016 年起持续积极维护
商业支持无(公司 2016 年关停)有高级插件与活跃开发
许可证Apache 2.0Apache 2.0

常见问题

RxDB 能否复制到 RethinkDB 后端?

RxDB 没有原生的 RethinkDB 复制插件。如果你在服务器上运行 RethinkDB,可以在其前面构建自定义 HTTP 或 WebSocket API,再用 RxDB 的自定义复制或 WebSocket 复制插件进行同步。RxDB 的复制协议只要求后端能按给定 checkpoint 提供文档变更并接受推送的文档,任何带 RethinkDB 驱动的服务端语言都能暴露这一接口。

RxDB 的响应式与 RethinkDB 的 changefeed 有何区别?

RethinkDB 的 changefeed 从服务器向客户端推送单个变更事件(旧值和新值),客户端收到的是原始事件,必须自行从中维护状态。而 RxDB 的响应式查询在每次相关变更后发射完整、最新的结果集:当查询匹配 10 个文档且其中一个更新时,订阅者收到的是全部 10 个当前文档。这直接对应 UI 渲染——你始终拥有完整状态,而不是一串需要自行应用的增量。event-reduce 算法通过从变更事件直接计算结果集更新、避免对存储重跑完整查询,让这一过程保持高效。

RxDB 适合实时协作应用吗?

适合。RxDB 已用于生产环境的协作应用:本地数据库保证 UI 始终即时响应,复制让所有客户端保持同步。对多个用户并发编辑同一文档的场景,RxDB 同时支持自定义冲突处理器和基于 CRDT 的合并;SharedWorker 存储模式则处理同一会话内多个浏览器标签页共享状态、避免重复的问题。

用户重新联网后 RxDB 如何处理重连?

RxDB 的复制插件持续运行并自动重试。网络不可用时,pull 和 push 处理器会失败,RxDB 等待retryTime毫秒后重试;网络恢复后,复制从最后一个成功的 checkpoint 自动继续。不会有任何变更丢失:离线期间的写入都存储在本地,连接重建后立即推送到服务器。

RxDB 需要登录或用户账户才能工作吗?

不需要。本地数据库在无任何认证的情况下即可工作。只有复制处理器需要凭据,而它们就是普通的 async 函数,你在其中携带后端要求的任意 header 或 token。如果应用运行期间认证过期,复制会暂停,你可以重新提供凭据并继续,无需重启数据库。

  • 数据库
  • NoSQL
  • 嵌入式数据库
  • 实时数据库

【免费下载链接】rxdb

The 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
点击查看免费下载

相关推荐

上一篇:终极调试神器LLDB-QuickLook:10个快速可视化调试技巧
下一篇:浏览器端AI抠图革命:无需服务器,3行代码实现专业级背景移除

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询