- 后端
- 数据库
【免费下载链接】sharedb
Realtime database backend based on Operational Transformation (OT)
ShareDB 致力于保持"数据库无关"(database-agnostic)的核心设计:任何数据的持久化、实例间变更通知、历史快照存储,都必须通过对应的**适配器(Adapter)**完成。本文以官方文档 docs/adapters/index.md 为骨架,结合仓库源码逐层拆解三类适配器(数据库适配器、Pub/Sub 适配器、Milestone 适配器)的职责边界、可用实现、源码原理与真实用法,帮助读者在测试、单机生产与多实例集群场景中正确选型并完成Backend的装配。
一、为什么 ShareDB 需要适配器:一次理解三类插槽
ShareDB 的核心思想是让实时协同引擎与底层存储、通信基础设施解耦。官方文档的表述非常直白:"ShareDB 试图保持数据库无关,因此任何持久化到数据库的数据都必须通过合适的数据库适配器完成。"这种解耦带来两个直接好处:
- 核心 OT(Operational Transformation)逻辑、客户端协议、中间件管线与底层存储完全隔离;
- 用户可以针对不同场景(内存测试、MongoDB、PostgreSQL、Redis 多实例)自由组合适配器。
从 lib/index.js 可以看到,ShareDB 将适配器相关的基类与内置实现全部挂载在Backend上对外暴露:Backend.DB、Backend.PubSub、Backend.MilestoneDB,以及内置的Backend.MemoryDB、Backend.MemoryPubSub、Backend.MemoryMilestoneDB。这意味着即使不安装任何第三方适配器包,require('sharedb')之后也可以直接拿到全套内置实现。
在 lib/backend.js 的构造函数中,三个适配器插槽的默认值一目了然:
this.db = options.db || new MemoryDB(); this.pubsub = options.pubsub || new MemoryPubSub(); this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB();即:数据库默认是内存版(不持久化)、Pub/Sub 默认是内存版(单实例可用)、Milestone 默认是空操作版(不存历史快照)。这正是后文逐个适配器讨论的起点。
二、数据库适配器(Database adapters):持久化文档内容与 op
2.1 职责边界
官方文档定义:数据库适配器负责持久化文档内容(snapshot)与操作记录(ops)。它处于 OT 提交链路的末端——当一个 op 通过中间件、完成变换与冲突消解后,最终会由db.commit(collection, id, op, snapshot, options, callback)写入底层存储;当客户端首次订阅或请求历史时,则由db.getSnapshot()与db.getOps()负责回读。
从 lib/db/index.js 这个基类可以看到适配器必须遵循的约定:
pollDebounce:查询轮询的最小间隔(毫秒),用于控制查询订阅的轮询频率;projectsSnapshots = false:为false时投影(projection)由 Backend 处理,若数据库适配器自身支持投影可将它置为true;disableSubscribe = false:数据库是否禁用订阅能力;- 一系列以
ERR_DATABASE_METHOD_NOT_IMPLEMENTED报错的"抽象方法":commit、getSnapshot、getOps、query、queryPollDoc等,任何第三方数据库适配器都必须实现这些核心方法(见 lib/db/index.js)。
2.2 官方文档列出的可选实现
| 适配器 | 后端存储 | 查询支持 | 适用场景 |
|---|---|---|---|
MemoryDB(内置) | 进程内存 | 无 | 测试、示例、适配器 API 参考实现 |
ShareDBMongo(sharedb-mongo) | MongoDB | 完整查询支持 | 生产环境首选 |
ShareDBMingoMemory(sharedb-mingo-memory) | 内存 | Mongo 操作子集(含查询) | 针对"类 MongoDB"实例做测试 |
ShareDBPostgres(sharedb-postgres) | PostgreSQL | 无查询支持 | 生产环境、无查询需求 |
文档特别给出两条重要提示:
MemoryDB不持久化,数据在应用重启后即丢失,绝不适用于生产环境;ShareDBMingoMemory在内存中实现了 Mongo 查询的一个子集,可以在本地测试环境中模拟"带查询的 MongoDB 实例"。
2.3 源码深挖:MemoryDB 的内部实现
内置的MemoryDB位于 lib/db/memory.js,其注释明确说明它是"测试与适配器实现参考"用途,并坦承三个局限:内存占用无限增长、无法跨 Node 进程扩展、服务重启数据全丢。
它的内部数据结构是两个嵌套 Map:
this.docs = Object.create(null); // collection -> id -> snapshot {v, type, data} this.ops = Object.create(null); // collection -> id -> op 数组(下标即版本号)几个值得关注的实现细节:
- 版本冲突检测:
commit会校验snapshot.v === 当前版本 + 1,不一致时以succeeded = false回调,由 Backend 触发重试(见 lib/db/memory.js)。这保证 OT 提交的原子性语义; - op 版本即数组下标:ops 以数组存储、版本号由下标推导,
getOps(collection, id, from, to)做一次slice即可返回,语义为[from, to)左闭右开(见 lib/db/memory.js); - "无查询支持"的真相:
query()默认返回集合内全部文档,真正决定查询语义的是_querySync(snapshots, query, options)——该函数默认原样返回全部快照,但注释明确说明:做测试时,可以通过覆写_querySync来实现你想要的查询语言(见 lib/db/memory.js)。这就是MemoryDB"没有查询支持" 的源码级含义:不是不能查,而是默认不做任何过滤。
测试用例 test/db-memory.js 则验证了基类的契约行为:直接实例化DB基类并调用commit/getSnapshot/getOps/query/queryPollDoc,都会以ERR_DATABASE_METHOD_NOT_IMPLEMENTED错误回调——这是所有第三方适配器必须实现的方法清单。
2.4 使用方式
数据库适配器实例通过Backend()构造函数的db选项注入(详见 Backend 构造文档):
const Backend = require('sharedb'); const MemoryDB = Backend.MemoryDB; const backend = new Backend({ db: new MemoryDB(), });如果省略db选项,Backend 会自动创建MemoryDB实例——这也是 examples/counter/server.js 中new ShareDB()直接可用、无需任何外部依赖的原因。切换到生产环境时,只需替换为sharedb-mongo的实例即可,业务代码零改动。
三、Pub/Sub 适配器:实例间的变更通知通道
3.1 职责边界
Pub/Sub 适配器负责向其他 ShareDB 实例通知数据的变更。在 OT 提交链路中,submit成功提交后会调用backend.pubsub.publish(channels, op)将 op 广播到相关频道(见 lib/submit-request.js),其他实例收到后即可把变更推送给各自连接的客户端。因此它决定了系统能否横向扩展为多实例。
3.2 官方文档列出的可选实现
MemoryPubSub(内置):进程内存实现,适用于单一、独立运行的 ShareDB 实例;ShareDBRedisPubSub(sharedb-redis-pubsub):基于 Redis;ShareDBWSBusPubSub(sharedb-wsbus-pubsub):基于 ws-bus。
与数据库适配器形成鲜明对比的是,文档用.info强调了一条关键结论:内存版 Pub/Sub 适配器在"仅运行单个独立 ShareDB 实例"的前提下是可以用于生产环境的。原因在 lib/pubsub/memory.js 的注释中写得很清楚:ShareDB 不要求持久化 Pub/Sub 状态,单进程内部的消息路由完全够用;将来要扩展多进程时,再平滑地替换为外部 Pub/Sub 适配器即可,且没有任何 Pub/Sub API 是适配器特有的。
3.3 源码深挖:PubSub 基类与频道机制
基类PubSub位于 lib/pubsub/index.js,其内部维护了三个关键状态:
streams:channel -> stream id ->OpStream的映射,一个频道可以挂多个订阅流;subscribed:已订阅频道的标记(与 streams 分开跟踪,因为流是同步创建、而订阅要等底层回调确认);prefix:可选前缀,subscribe/publish时自动拼接为prefix + ' ' + channel,用于隔离命名空间(见 lib/pubsub/index.js)。
基类同样通过ERR_DATABASE_METHOD_NOT_IMPLEMENTED定义抽象接口_subscribe、_unsubscribe、_publish(见 lib/pubsub/index.js),对外则统一暴露subscribe(channel, callback)(返回OpStream)与publish(channels, data, callback)。MemoryPubSub的实现极为轻量——_publish只做一件事:遍历频道、把数据推给该频道下所有已订阅流(见 lib/pubsub/memory.js)。
3.4 使用方式
const Backend = require('sharedb'); const MemoryPubSub = Backend.MemoryPubSub; const backend = new Backend({ pubsub: new MemoryPubSub(), });多实例场景下,只需把MemoryPubSub换成ShareDBRedisPubSub实例(Redis 负责跨进程广播),即可让多个 ShareDB 进程协同工作。
四、Milestone 适配器:用周期性快照加速文档历史
4.1 职责边界与背景
Milestone 适配器负责存储文档的周期性快照(Milestone Snapshots),核心目的是加速 文档历史查询。背景如下:ShareDB 默认保存全部 op,重建历史快照时需要从创建版本开始逐条重放 op;文档版本一旦很高,重放会变得很慢。Milestone 快照的存在让 ShareDB 可以"跳到最近的一个快照,再从这个快照继续重放",把成本降到可接受范围(见 docs/document-history.md)。
4.2 官方文档列出的可选实现
ShareDBMilestoneMongo(sharedb-milestone-mongo):基于 MongoDB。
文档同时说明:Milestone 适配器的默认行为可以通过中间件覆写(详见下文 4.4)。仓库内置的MemoryMilestoneDB(lib/milestone-db/memory.js)虽然文档未在"可用适配器"中列明,但其注释指出:Milestone 概念依赖持久化,内存版不适用于生产,主要作为实现范例与测试用途。
4.3 源码深挖:接口、interval 与 NoOp 默认实现
基类MilestoneDB(lib/milestone-db/index.js)在构造函数中接收interval选项,并定义四个核心方法:
getMilestoneSnapshot(collection, id, version):返回版本号小于或等于目标 version 的最近一个快照;saveMilestoneSnapshot(collection, snapshot):保存一个快照;getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp):按时间戳取快照(用于按时间查询历史);getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp):时间戳语义的补充接口。
"多久存一个快照"的默认逻辑在 lib/submit-request.js 的_shouldSaveMilestoneSnapshot中:
// 如果 saveMilestoneSnapshot 为 null(未被覆写),按 milestoneDb 的 interval 决定 if (this.saveMilestoneSnapshot === null) { return snapshot && snapshot.v % this.milestoneDb.interval === 0; } return this.saveMilestoneSnapshot;即默认规则是**"版本号能被 interval 整除时保存"**,且从 lib/backend.js 可知:milestoneDb选项省略时默认注入NoOpMilestoneDB——lib/milestone-db/no-op.js 中说明它是"静默的空操作默认实现",所有方法立即回调空结果。这与 Backend 构造文档 的说明一致:省略该选项则 Milestone 快照不启用,文档历史依然可查,但可能付出性能代价。
在读取侧,lib/backend.js 的_fetchSnapshot展示了 Milestone 如何参与历史重建:先milestoneDb.getMilestoneSnapshot()找到最近的基点,再db.getOps(collection, id, from, version)从该基点重放到目标版本,最后ot.applyOps重建快照。测试 test/milestone-db.js 也验证了interval: 2时"只保存偶数版本"、按版本 1/2/3/4 查询分别返回 undefined/2/2/4 的行为。
4.4 用中间件覆写保存策略
官方文档给出的核心实战能力是:在commit中间件中设置context.saveMilestoneSnapshot来覆写默认的 interval 逻辑——true表示请求保存快照,false表示不保存,保持null(默认值)则沿用适配器默认行为。完整的官方示例(按集合定制保存频率):
shareDb.use('commit', (context, next) => { switch (context.collection) { case 'foo': // 集合 'foo':每 100 个版本保存一次 context.saveMilestoneSnapshot = context.snapshot.v % 100 === 0; break; case 'bar': case 'baz': // 集合 'bar' 与 'baz':每 500 个版本保存一次 context.saveMilestoneSnapshot = context.snapshot.v % 500 === 0; break; default: // 其他集合:完全不保存 Milestone context.saveMilestoneSnapshot = false; } next(); });对应地,测试 test/milestone-db.js 验证了中间件覆写路径:request.saveMilestoneSnapshot = request.snapshot.v >= 3时,版本 1、2 不保存,版本 3 起开始保存。需要说明的是,commit是 op 提交前的最后一个中间件钩子(详见 中间件动作文档 与 op 提交流程),此时context.snapshot已经是"新状态"——即 op 应用后的快照,因此snapshot.v代表提交后的新版本号。
4.5 使用方式
const Backend = require('sharedb'); const ShareDBMilestoneMongo = require('sharedb-milestone-mongo'); const backend = new Backend({ milestoneDb: new ShareDBMilestoneMongo(), });五、选型指南:三种适配器的组合决策
综合官方文档的约束与源码事实,可以将选型归纳为一张决策表:
| 场景 | db | pubsub | milestoneDb |
|---|---|---|---|
| 单元测试 / 示例代码 | MemoryDB(默认) | MemoryPubSub(默认) | 不配(默认 NoOp)或MemoryMilestoneDB |
| 单实例生产 | sharedb-mongo等持久化适配器 | MemoryPubSub(官方明确允许) | sharedb-milestone-mongo(需要历史加速时) |
| 多实例集群 | sharedb-mongo等持久化适配器 | sharedb-redis-pubsub等外部实现 | sharedb-milestone-mongo |
几点关键事实需要牢记:
MemoryDB不持久化、无查询、不跨进程、内存无界增长,只能用于测试与作为适配器参考实现(lib/db/memory.js);- 查询能力依赖数据库适配器:只有
sharedb-mongo(完整)与sharedb-mingo-memory(Mongo 子集)支持查询,sharedb-postgres与MemoryDB不支持——若业务需要查询订阅(查询文档),选型时必须先确认适配器是否实现query方法; MemoryPubSub单实例生产可用,这是与内存数据库截然不同的结论,源自"Pub/Sub 状态无需持久化"这一事实(lib/pubsub/memory.js);- Milestone 是可选的性能优化,其默认行为由
milestoneDb.interval驱动(v % interval === 0时保存),需要更细粒度控制时用commit中间件覆写context.saveMilestoneSnapshot即可(lib/submit-request.js)。
六、总结
ShareDB 的适配器体系用三个正交的插槽完成了"存储、通信、历史优化"三件事的彻底解耦:数据库适配器负责文档与 op 的持久化,Pub/Sub 适配器负责实例间变更广播,Milestone 适配器负责周期快照以加速历史重建。三者均通过Backend()构造函数的db、pubsub、milestoneDb选项注入,官方同时提供了开箱即用的内存实现与覆盖 MongoDB、Redis 等生态的第三方实现。理解这套体系,是正确部署 ShareDB(从单机测试到多实例生产)的前提——选型时请始终对照"是否持久化、是否支持查询、是否跨实例、是否加速历史"这四个问题。
- 后端
- 数据库
【免费下载链接】sharedb
Realtime database backend based on Operational Transformation (OT)
相关推荐
ShareDB Pub/Sub 适配器(Pub/Sub Adapters)详解:多实例实时同步的发布订阅层
ShareDB Pub/Sub 适配器(Pub/Sub Adapters)详解:多实例实时同步的发布订阅层 ShareDB 的 Pub/Sub 适配器负责在数据
后端数据库ShareDB Milestone 适配器实战指南:用周期性快照加速文档历史重建
ShareDB Milestone 适配器实战指南:用周期性快照加速文档历史重建 Milestone 适配器(milestone adapter)是 Share
后端数据库ShareDB 数据库适配器(Database Adapters)完全指南:从 MemoryDB 到 MongoDB / PostgreSQL
ShareDB 数据库适配器(Database Adapters)完全指南:从 MemoryDB 到 MongoDB / PostgreSQL ShareDB
后端数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考