SpreadJS 协同服务器 MongoDB 数据库适配支持

为了支持 SpreadJS 协同编辑场景,协同服务器需要持久化存储文档、操作、快照及里程碑数据。本文介绍了 MongoDB 数据库适配器的实现方法,包括集合初始化、适配器接口实现以及里程碑存储支持。

一、MongoDB 集合初始化

协同编辑服务需要以下集合(Collections)来存储数据:

  • documents:存储文档基本信息(类型、版本号、快照版本号)。
  • operations:存储操作记录,用于实现基于 OT 的操作重放。
  • snapshot_fragments:存储快照的分片数据,支持大文档分段管理。
  • milestone_snapshot:存储里程碑快照,用于回溯和恢复。

初始化脚本示例如下:

export async function InitCollections(client) {const collectionsToCreate = [{ name: 'documents', indexes: [{ key: { id: 1 }, unique: true }] },{ name: 'operations', indexes: [{ key: { doc_id: 1, version: 1 }, unique: true }] },{ name: 'snapshot_fragments', indexes: [{ key: { doc_id: 1, fragment_id: 1 }, unique: true }] },{ name: 'milestone_snapshot', indexes: [{ key: { doc_id: 1, version: 1 }, unique: true }] },];await client.connect();const db = client.db(dbName);
const existingCollections = (await db.listCollections().toArray()).map(c => c.name);
for (const col of collectionsToCreate) {if (!existingCollections.includes(col.name)) {await db.createCollection(col.name);console.log(`Collection '${col.name}' created.`);} else {console.log(`Collection '${col.name}' already exists.`);}for (const idx of col.indexes) {await db.collection(col.name).createIndex(idx.key, { unique: idx.unique });}}
await client.close();
}

二、MongoDB 适配器实现

  1. 适配器说明

适配器 MongoDb 继承了协同服务的数据库接口,负责:

  • 文档信息存取
  • 操作记录管理
  • 快照存储与更新
  • 分片快照管理

可根据业务需要,增加 事务(session)并发冲突检测

  1. 适配器核心实现
export class MongoDb extends Db {constructor(client) {super();this.client = client;this.client.connect()this.db = client.db(dbName);}async getDocument(docId) {const documents = this.db.collection('documents');let row = await documents.findOne({ id: docId });if (row) {return {id: row.id,type: row.type,version: row.version,snapshotVersion: row.snapshot_version};}}async getSnapshot(docId) {const documents = this.db.collection('documents');let row = await documents.findOne({ id: docId });if (!row) {return null;}const fragments = await this.getFragments(docId);return {id: row.id,v: row.snapshot_version,type: row.type,fragments: fragments};}async getFragments(docId) {const fragments = this.db.collection('snapshot_fragments');const rows = await fragments.find({ doc_id: docId }).toArray();if (rows.length === 0) {return {};}const results = {};for (const row of rows) {results[row.fragment_id] = JSON.parse(row.data);}return results;}async getFragment(docId, fragmentId) {const fragments = this.db.collection('snapshot_fragments');const row = await fragments.findOne({ doc_id: docId, fragment_id: fragmentId });if (row) {return JSON.parse(row.data);}return null;}async getOps(docId, from, to) {const operations = this.db.collection('operations');const query = { doc_id: docId, version: { $gte: from } };if (to !== undefined) {query.version.$lte = to;}const rows = await operations.find(query).toArray();if (rows.length === 0) {return [];}return rows.map(row => JSON.parse(row.operation));}async commitOp(docId, op, document) {try {const documents = this.db.collection('documents');const operations = this.db.collection('operations');const row = await documents.findOne({ id: docId });if (op.create) {if (row) {throw new Error(`Document with id ${docId} already exists.`);}await documents.insertOne({id: docId,type: document.type,version: document.version,snapshot_version: document.snapshotVersion},);await operations.insertOne({doc_id: docId,version: op.v,operation: JSON.stringify(op)},);return true;}else if (op.del) {if (!row) {throw new Error(`Document with id ${docId} does not exist.`);}await documents.deleteOne({ id: docId },);return true;}else {if (!row || row.version !== op.v) {throw new Error(`Document with id ${docId} does not exist or version mismatch.`);}await operations.insertOne({doc_id: docId,version: op.v,operation: JSON.stringify(op)},);await documents.updateOne({ id: docId },{ $set: { version: document.version } },);return true;}}catch (error) {console.error('Error committing operation:', error);return false;}finally {}}async commitSnapshot(docId, snapshot) {try {const documents = this.db.collection('documents');const fragments = this.db.collection('snapshot_fragments');const row = await documents.findOne({ id: docId },);if (!row) {throw new Error(`Document with id ${docId} does not exist.`);}const currentSnapshotVersion = row.snapshot_version;if (snapshot.fromVersion !== currentSnapshotVersion || snapshot.v <= currentSnapshotVersion) {throw new Error(`Snapshot version mismatch: expected ${currentSnapshotVersion}, got ${snapshot.v}`);}await documents.updateOne({ id: docId },{ $set: { snapshot_version: snapshot.v } },);if (snapshot.fragmentsChanges.deleteSnapshot) {fragments.deleteMany({ doc_id: docId },);}else {const { createFragments, updateFragments, deleteFragments } = snapshot.fragmentsChanges;if (createFragments) {const createOps = Object.entries(createFragments).map(([id, data]) => ({doc_id: docId,fragment_id: id,data: JSON.stringify(data)}));if (createOps.length > 0) {await fragments.insertMany(createOps,);}}if (updateFragments) {const updateOps = Object.entries(updateFragments).map(([id, data]) => ({updateOne: {filter: { doc_id: docId, fragment_id: id },update: { $set: { data: JSON.stringify(data) } }}}));if (updateOps.length > 0) {await fragments.bulkWrite(updateOps,// { session });}}if (deleteFragments) {const deleteOps = deleteFragments.map(id => ({deleteOne: {filter: { doc_id: docId, fragment_id: id }}}));if (deleteOps.length > 0) {await fragments.bulkWrite(deleteOps,);}}}return true;}catch (error) {console.error('Error committing snapshot:', error);return false;}finally {}}async close() {await this.client.close();}
}

三、里程碑数据存储

里程碑快照用于优化快照恢复性能(避免从头重放所有操作)。 实现类 MongoMilestoneDb 提供 保存读取 接口:

export class MongoMilestoneDb {constructor(client, interval) {this.client = client;this.interval = interval ? interval : 1000;this.db = client.db(dbName);}
async saveMilestoneSnapshot(snapshot) {const milestones = this.db.collection('milestone_snapshot');await milestones.insertOne({doc_id: snapshot.id,version: snapshot.v,snapshot: JSON.stringify(snapshot)});return true;}
async getMilestoneSnapshot(id, version) {const milestones = this.db.collection('milestone_snapshot');const row = await milestones.findOne({ doc_id: id, version: { $lte: version } },{ sort: { version: -1 } });if (row) {return JSON.parse(row.snapshot);}return null;}
}

四、在 DocumentServices 中配置

完成适配器与里程碑数据库后,需要在 DocumentServices 中进行配置:

const documentServices = new OT.DocumentServices(
{ db: new MongoDb(mongoClient),milestoneDb: new MongoMilestoneDb(mongoClient, 500)
});

这样,SpreadJS 协同服务器即可通过 MongoDB 实现文档存储、操作日志管理、快照与里程碑维护,保证协同编辑过程的高效与可扩展。

五、总结

本文展示了 SpreadJS 协同服务器对 MongoDB 数据库的适配实现,主要包括:

  1. 集合初始化:定义所需集合与索引。
  2. 数据库****适配器:支持文档、操作、快照的存储与管理。
  3. 里程碑存储:提供快照的高效回溯能力。
  4. 服务集成:在 DocumentServices 中配置 MongoDB 适配器。

借助 MongoDB 的高性能与灵活数据结构,SpreadJS 协同服务可实现稳定、可扩展的文档协作平台。

扩展链接

使用 MySQL 为 SpreadJS 协同服务器提供存储支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94032.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu 主机名:精通配置与管理

主机名&#xff08;hostname&#xff09;是Linux系统中用于标识网络上特定设备的名称&#xff0c;它在网络通信、服务配置&#xff08;如 Kubernetes 集群、数据库&#xff09;以及日志记录中扮演着至关重要的角色。对于初学者来说&#xff0c;配置主机名似乎很简单&#xff0c…

C/C++ 协程:Stackful 手动控制的工程必然性

&#x1f680; C/C 协程&#xff1a;Stackful 手动控制的工程必然性 引用&#xff1a; C/C 如何正确的切换协同程序&#xff1f;&#xff08;基于协程的并行架构&#xff09; #mermaid-svg-SXgplRf3WRYc8A7l {font-family:"trebuchet ms",verdana,arial,sans-serif;…

新手向:使用STM32通过RS485通信接口控制步进电机

新手向&#xff1a;使用STM32通过RS485通信接口控制步进电机 准备工作 本文使用的STM32芯片是STM32F407ZGTx&#xff0c;使用的电机是57步进电机&#xff0c;驱动器是用的是时代超群的RS485总线一体化步进电机驱动器&#xff08;42 型&#xff1a;ZD-M42P-485&#xff09;。使…

设计模式笔记_行为型_命令模式

1.命令模式介绍命令模式&#xff08;Command Pattern&#xff09;是一种行为设计模式&#xff0c;它将请求或操作封装为对象&#xff0c;使得可以用不同的请求对客户端进行参数化。命令模式的核心思想是将方法调用、请求或操作封装到一个独立的命令对象中&#xff0c;从而使得客…

详解MySQL中的多表查询:多表查询分类讲解、七种JOIN操作的实现

精选专栏链接 &#x1f517; MySQL技术笔记专栏Redis技术笔记专栏大模型搭建专栏Python学习笔记专栏深度学习算法专栏 欢迎订阅&#xff0c;点赞&#xff0b;关注&#xff0c;每日精进1%&#xff0c;与百万开发者共攀技术珠峰 更多内容持续更新中&#xff01;希望能给大家带来…

vue3+elemeent-plus, el-tooltip的样式修改不生效

修改后的样式&#xff0c;直接贴图&#xff0c;经过删除出现悬浮1、在书写代码的时候切记effect“light”&#xff0c;如果你需要的是深色的样式:disabled"!multiple" 是否禁用<el-tooltip effect"light" placement"top" content"请先选…

网页作品惊艳亮相!这个浪浪山小妖怪网站太治愈了!

大家好呀&#xff01;今天要给大家分享一个超级治愈的网页作品——浪浪山小妖怪主题网站&#xff01;这个纯原生开发的项目不仅颜值在线&#xff0c;功能也很能打哦&#xff5e;至于灵感来源的话&#xff0c;要从一部动画说起。最近迷上了治愈系动画&#xff0c;就想做一个温暖…

搭建最新--若依分布式spring cloudv3.6.6 前后端分离项目--步骤与记录常见的坑

首先 什么拉取代码&#xff0c;安装数据库&#xff0c;安装redis&#xff0c;安装jdk这些我就不说了 导入数据库 &#xff1a;数据库是分库表的 &#xff0c;不要建错了 【一定要注意&#xff0c;不然nacos读取不到配置文件】这个是给nacos用的这个是给项目配置或项目用的2. 服…

分布式唯一 ID 生成方案

在复杂分布式系统中&#xff0c;往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中&#xff0c;数据日渐增长&#xff0c;对数据分库分表后需要有一个唯一 ID 来标识一条数据或消息&#xff0c;数据库的自增 ID 显然不能…

飞算JavaAI赋能高吞吐服务器模拟:从0到百万级QPS的“流量洪峰”征服之旅

引言&#xff1a;当“流量洪峰”来袭&#xff0c;如何用低代码驯服高并发&#xff1f; 在数字化时代&#xff0c;从电商平台的“双11”大促到社交网络的突发热点事件&#xff0c;再到金融系统的实时交易高峰&#xff0c;服务器时刻面临着**高吞吐量&#xff08;High Throughput…

C#数据访问帮助类

一.中文注释using System; using System.Data; using System.Xml; using System.Data.SqlClient; using System.Collections;namespace Microsoft.ApplicationBlocks.Data.Ch {/// <summary>/// SqlServer数据访问帮助类/// </summary>public sealed class SqlHelp…

B站 韩顺平 笔记 (Day 21)

目录 1&#xff08;面向对象高级部分练习题&#xff09; 1.1&#xff08;题1&#xff09; 1.2&#xff08;题2&#xff09; 1.3&#xff08;题3&#xff09; Vehicles接口类&#xff1a; Horse类&#xff1a; Boat类&#xff1a; Plane类&#xff1a; VehiclesFactory…

Linux(十四)——进程管理和计划任务管理

文章目录前言一、程序与进程的关系1.1 程序与进程的定义1.2 父进程与子进程二、查看进程信息2.1 ps 命令&#xff08;重点&#xff09;2.2 动态查看进程信息top命令&#xff08;重点&#xff09;2.3 pgrep命令查询进程信息2.4 pstree命令以树形结构列出进程信息三、进程的启动方…

太阳光模拟器在无人机老化测试中的应用

在无人机技术飞速发展的当下&#xff0c;其户外作业环境复杂多变&#xff0c;长期暴露在阳光照射下&#xff0c;部件老化问题日益凸显&#xff0c;严重影响无人机的性能与寿命。紫创测控Luminbox专注于太阳光模拟器技术创新与精密光学测试系统开发&#xff0c;其涵盖的 LED、卤…

网络原理-TCP_IP

1.UDP&#xff08;即用户数据报协议&#xff09;UDP是一种无连接的传输层协议&#xff0c;提供简单的、不可靠的数据传输服务。它不保证数据包的顺序、可靠性或重复性&#xff0c;但具有低延迟和高效率的特点。UDP协议段格式16位UDP⻓度,表⽰整个数据报(UDP⾸部UDP数据)的最⼤⻓…

GitHub Actions YAML命令使用指南

version: 2 updates:- package-ecosystem: "github-actions"directory: "/"schedule:interval: "weekly"这段代码是 Dependabot 的配置文件&#xff08;通常放在 .github/dependabot.yml 中&#xff09;&#xff0c;它的作用是 自动化管理 GitHu…

决策树算法学习总结

一、经典决策树算法原理 &#xff08;一&#xff09;ID3 算法 核心思想&#xff1a;以 “信息增益” 作为划分属性的选择标准&#xff0c;通过最大化信息增益来提升数据集的 “纯度”。 关键概念 —— 信息增益&#xff1a;指某个属性带来的 “熵减”&#xff08;即纯度提升量&…

内网安全——出网协议端口探测

在实战中难免会遇到各种各样的情况&#xff0c;其中对于目标主机是否出网这是一个十分值得收集的信息&#xff0c;因为完全不出网你就获取不到主机了 端口 Linux 系统 对于 Linux 系统&#xff0c;探测其允许出网的端口&#xff0c;这里使用的是 Linux 的自带命令&#xff0c;所…

C#WPF实战出真汁13--【营业查询】

1、营业查询介绍本模块是最后一个模块&#xff0c;该板块需要的功能有&#xff1a;营业数据列表&#xff0c;查询数据&#xff0c;导出数据&#xff0c;数据统计。2、UI设计布局TabControl 是 WPF 中用于创建多页标签式界面的控件&#xff0c;常用于组织多个子内容区域。每个子…

基于 Java 和 MySQL 的精品课程网站

基于 Java 和 MySQL 的精品课程网站设计与实现一、 毕业设计&#xff08;论文&#xff09;任务书摘要&#xff1a;近年来&#xff0c;教育信息化发展十分迅猛&#xff0c;人们的教育观念、教育手段、学习方法、学习渠道等等都发生了重大的变化。知识性人才也已经日益成为了一个…