文档协同技术笔记
概述
什么是文档协同?
文档协同,是指 多个用户 在 不同的设备 对 同一份文档 进行实时编辑协作。
需要解决的问题
- 防止冲突:多用户同时编辑时避免数据覆盖
- 数据一致性:确保所有用户看到相同的文档状态
- 实时同步:将修改即时同步给所有协作者
常见解决方案
| 方案 | 实时性 | 并发编辑 | 实现复杂度 | 人工干预 |
|---|---|---|---|---|
| 读写锁 | 低 | ❌ 不支持 | ⭐ 简单 | ❌ |
| diff-patch | 中 | ⚠️ 支持但需处理冲突 | ⭐⭐ 中等 | ⚠️ 冲突时需要 |
| OT | 高 | ✅ 支持 | ⭐⭐⭐ 复杂 | ❌ |
| CRDT | 高 | ✅ 支持 | ⭐⭐⭐ 复杂 | ❌ |
方案一:读写锁
核心概念
读写锁通过 写锁(Write Lock) 和 读锁(Read Lock) 管理文档访问权限:
- 写锁:获取写锁的用户独占编辑权,其他用户无法修改
- 读锁:文档被编辑时,其他用户仍可只读访问
与操作系统读写锁的区别
| 类型 | 写锁时 | 读锁时 |
|---|---|---|
| 操作系统 | 禁止读和写 | 允许并发读,禁止写 |
| 文档协同 | 允许只读 | 允许并发读,允许写锁排队 |
优缺点
优点:
- 实现简单
- 完全避免冲突
- 读取具有并发性
缺点:
- 写锁期间其他用户只能等待,体验差
- 可能造成饥饿现象(长时间占锁)
使用场景
- 对实时性要求不高
- 内容稳定、变动频率低(90%查看,10%编辑)
- 强调强一致性(如审批流程文档)
方案二:diff-patch 合并
核心概念
通过计算文档差异来合并不同用户的修改,类似 Git 的合并流程:
- diff:比较多个用户的编辑内容与原始文档的差异
- patch:生成格式化更新补丁
- 冲突检测:无法自动合并时提示用户手动解决
优缺点
优点:
- 实现相对简单
- 支持并发编辑
- 版本可追溯
- 带宽占用小
缺点:
- 冲突需要人工处理
- 合并精度有限
- 冲突区域难以管理
使用场景
- 小范围协作编辑
- 文档结构稳定
- 异步协作与离线编辑
- 注重版本可追溯性
小结
diff-patch 是一种轻量级、实现成本低的文档协同策略,适用于修改频率适中、协同规模有限的 非实时协作场景。虽然高频编辑时人工干预成本较高,但作为"Git 风格"协作的核心组件,仍被广泛应用于代码编辑器、CMS 等工具中。
方案三:OT 算法(Operational Transformation)
发展史
| 年份 | 里程碑 |
|---|---|
| 1989 | OT 算法正式提出,标志协同编辑技术进步 |
| 2006 | Google 首次将 OT 应用于商业产品 Google Docs |
| 2011 | 微软在 Office 365 中基于 OT 实现协同编辑 |
| 2012 | Quill 编辑器开源,其数据模型 Delta 基于 OT 设计 |
| 2013 | ShareDB 开源,基于 OT 的流行解决方案 |
核心思想
OT = 操作(Operation)+ 转换(Transformation)
每个用户对文档的操作被记录并传播给其他用户,通过转换操作保证最终文档一致性。
1. 操作
文档的每次修改都是原子化操作,以 Quill 的 Delta 模型为例:
json
{
"ops": [
{ "insert": "Gandalf", "attributes": { "bold": true } },
{ "insert": " the " },
{ "insert": "Grey", "attributes": { "color": "#cccccc" } }
]
}
操作类型
| 操作 | 说明 | 示例 |
|---|---|---|
insert |
插入文本 | { "insert": "Hello" } |
retain |
保留(跳过)字符 | { "retain": 7, "attributes": { "bold": null } } |
delete |
删除字符 | { "delete": 4 } |
attributes:
null值表示移除格式- 对象表示应用格式
2. 转换
转换发生在 服务端,对多个客户端的操作进行变换和冲突修正。
具体示例
初始文档:|a|b|c|(索引 0,1,2)
用户A:在位置 1 插入 X
用户B:在位置 0 插入 Y
并发场景:
初始状态 abc
↓
┌──────────────┐
│ abc │
└──────────────┘
/ \
/ \
A: Insert(1,'X') B: Insert(0,'Y')
↓ ↓
aXbc Yabc
↓ ↓
OT 变换 OT 变换
↓ ↓
应用 B'→Insert(0,'Y') 应用 A'→Insert(2,'X')
↓ ↓
┌──────────┐ ┌──────────┐
│ YaXbc │ │ YaXbc │
└──────────┘ └──────────┘
✅ 一致 ✅ 一致
关键点
- 强一致性:OT 保证最终结果一致
- 依赖服务端:所有操作需在服务器转换
- 需要中央协调:不适合纯分布式系统
OT 的局限性
OT 是 强一致性设计,所有操作需在服务端转换,因此:
- 对网络要求高
- 不太适合分布式系统
- 网络异常可能导致转换失败
方案四:CRDT 算法(Conflict-free Replicated Data Type)
为什么需要 CRDT?
CRDT 是为了解决 OT 的局限性:
- OT 需要中央服务器协调,不适合分布式
- 网络异常时服务端转换会出问题
CAP 理论
CAP 理论由 Eric Brewer 提出:
- C(Consistency):一致性,所有节点访问数据一致
- A(Availability):可用性,系统始终响应请求
- P(Partition tolerance):分区容忍性,容忍网络故障
在分布式系统中,P 是必须的,只能在 C 和 A 中选择其一。
OT 选择 C(强一致性)
CRDT 选择 A(可用性)→ 追求最终一致性
核心思想
CRDT 的设计哲学:
只要所有副本 最终 都收到相同的信息,不管谁先谁后,最终状态一定一样。
数据结构设计
每个字符带有唯一 ID:
js
{
char: 'x', // 字符内容
id: [position_id, client_id] // 唯一标识
}
具体示例
初始文档 abc,各字符 ID:
| 字符 | ID | 说明 |
|---|---|---|
| a | [1, A] | 用户A写入 |
| b | [2, A] | - |
| c | [3, A] | - |
操作1:用户A在 a 后插入 X
生成新 ID [1.5, A]:
js
[ [1, A]: 'a', [1.5, A]: 'X', [2, A]: 'b', [3, A]: 'c' ]
操作2:用户B在开头插入 Y
生成新 ID [0.5, B]:
js
[ [0.5, B]: 'Y', [1, A]: 'a', [2, A]: 'b', [3, A]: 'c' ]
合并副本:按 ID 排序自动合并
js
[0.5, B] → [1, A] → [1.5, A] → [2, A] → [3, A]
Y → a → X → b → c
// 结果:YaXbc
数据结构特点
| 特点 | 公式 | 作用 |
|---|---|---|
| 可交换性 | merge(a,b) === merge(b,a) |
无视消息到达顺序 |
| 可结合性 | merge(merge(a,b),c) === merge(a,merge(b,c)) |
不限制合并顺序 |
| 幂等性 | merge(state, state) === state |
支持消息重发 |
冲突处理
CRDT 的数据结构设计确保 所有操作天然不冲突:
- 并发插入:按 ID 排序
- 并发删除:标记删除,最终状态不含该字符
- 合并:只需按 ID 排序,无需判断"谁赢谁输"
CRDT 实现对比
| 算法 | ID 结构 | 特点 |
|---|---|---|
| RGA | [前一字符ID, 副本ID] | 链表式插入 |
| Logoot | [路径ID, 用户ID] | ID 可排序且稀疏 |
| Yjs | 类似 Logoot + 优化 | 实际使用偏移机制 |
OT vs CRDT 对比
| 对比维度 | OT | CRDT |
|---|---|---|
| 一致性 | 强一致性 | 最终一致性 |
| 架构 | 需要中央服务器 | 去中心化 |
| 网络要求 | 高 | 低 |
| 离线支持 | ❌ 不支持 | ✅ 支持 |
| 冲突处理 | 服务端转换 | 客户端自动合并 |
| 复杂度 | 服务端复杂 | 客户端数据结构复杂 |
| 代表实现 | Google Docs, ShareDB | Yjs, Automerge |
选择建议:
- 需要强一致性、高实时性 → OT
- 需要离线支持、去中心化 → CRDT
实践:Yjs 框架
Yjs 是一个基于 CRDT 的高性能协作编辑框架。
架构设计
┌─────────────────────────────────────────────┐
│ 顶层:编辑器适配层 │
│ ProseMirror / Tiptap / Slate / Monaco │
├─────────────────────────────────────────────┤
│ 中间层:Yjs 核心 │
│ CRDT 数据结构(Y.Text, Y.Map, Y.Array) │
├─────────────────────────────────────────────┤
│ 底层:通信与存储模块 │
│ y-websocket / y-webrtc / y-indexeddb │
└─────────────────────────────────────────────┘
编辑器绑定
| 编辑器 | 绑定模块 |
|---|---|
| ProseMirror / Tiptap | y-prosemirror |
| Quill | y-quill |
| Monaco(代码编辑器) | y-monaco |
| CodeMirror | y-codemirror.next |
核心优势
- 冲突处理稳健:基于 CRDT,无需中央协调
- 协作者状态同步:内置 Awareness 模块,同步光标和用户状态
- 离线编辑支持:天然支持,配合 y-indexeddb 实现本地持久化
- 版本快照:轻量级快照机制,支持版本回溯
- 高并发:实测支持数十到上百人实时编辑
- 插件生态丰富:多种通信和存储模块可选
Yjs vs Automerge
| 对比点 | Automerge | Yjs |
|---|---|---|
| 性能 | 较慢(v2有改进) | 非常快 |
| 内存使用 | 较大(保留完整历史) | 较小(支持GC) |
| 社区活跃度 | 中等 | 高 |
| 插件生态 | 较少 | 丰富 |
| 适用场景 | 通用 | 编辑器方向 |
官网资源
- 官网 :https://yjs.dev/
- 文档:权威 API 和使用方式
- 通信模块:y-websocket、y-webrtc、y-indexeddb、y-redis
-EOF-