- 数据库
- 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/
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 / Expo | SQLite(expo-sqlite 或 op-sqlite) |
| Node.js / Electron | SQLite(better-sqlite3) |
| 多标签页浏览器 | SharedWorker |
| 测试 / CI | Memory |
切换存储只需在创建数据库时改一个参数:
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内部维护了received、sent、error、canceled、active、conflict共六个 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)); });以上全部离线可用。需要服务器同步时,接上一个复制插件即可。
对比总结
| 维度 | RethinkDB | RxDB |
|---|---|---|
| 运行位置 | 服务端集群 | 客户端(浏览器、移动端、桌面端) |
| 离线支持 | 无(必须联网) | 完整的离线优先 |
| 响应式查询 | 服务器推送 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.0 | Apache 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/
相关推荐
用 RxDB 作为 React 应用的客户端数据库:离线优先、实时响应与端到端同步实战指南
用 RxDB 作为 React 应用的客户端数据库:离线优先、实时响应与端到端同步实战指南 本文以 react database.md https://link
数据库NoSQL嵌入式数据库实时数据库RxDB 作为 AWS Amplify DataStore 替代方案:后端无关的离线优先数据库实践
RxDB 作为 AWS Amplify DataStore 替代方案:后端无关的离线优先数据库实践 AWS Amplify DataStore 曾经提供了一个诱
数据库NoSQL嵌入式数据库实时数据库Terraform Stacks 运行时内部架构解析:基于隐式数据流求值的声明式语言运行时
Terraform Stacks 运行时内部架构解析:基于隐式数据流求值的声明式语言运行时 本文面向 Terraform 的维护者与内核研究者,系统剖析 Sta
数据库NoSQL嵌入式数据库实时数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考