# 实时协同文档 架构模板 > **代表产品**:Google Docs、腾讯文档、飞书文档、Notion、Figma > **一句话定位**:让多个人同时编辑同一份文档,各自的改动实时合并、互不覆盖,最终人人看到完全一致的结果。 --- ## 1. 一句话定位 实时协同文档 = **每端一份本地副本** + **一个把多人编辑意图实时「合并成单一一致状态」的引擎**。 它的真正难点不在存储,而在一个看似简单的问题:**当两个人在同一秒、改同一句话时,怎么办?** 既不能用锁让一个人干等(那就不叫协同了),又不能让后保存的人覆盖掉前一个人(那是丢数据)。整套架构的灵魂,就是那套**在没有锁的情况下合并并发编辑、并保证所有人收敛到一致**的算法。 ## 2. 业务本质:它在解决什么问题 它消灭的是「**发文件 → 各自改 → 邮件发回 → 手动合并 → `最终版_v3_final_真的最终.docx`**」这套痛苦的来回。让协作从「异步接力」变成「同步共创」,所有人实时看到彼此在做什么。 钱从哪来:企业协作套件订阅、团队 / 空间席位、增值的权限与合规能力。**协作的「实时感」本身就是产品的护城河。** ## 3. 核心需求与约束 **功能性需求:** - [ ] 多人实时编辑同一文档 - [ ] 看到他人的光标 / 选区 / 在线状态(presence) - [ ] 离线编辑,联网后自动同步 - [ ] 历史版本与回溯 - [ ] 评论 / 批注 **非功能性需求 / 质量属性:** | 质量属性 | 目标 | 为什么对这类系统重要 | |---|---|---| | **实时性** | 改动亚秒级同步 | 协作的「同框感」全靠它 | | **收敛一致性** | 所有人最终完全一致 | 不允许两个人看到不同的文档 | | **冲突合并** | 不丢、不覆盖任何人的改动 | 这是协同的底线 | | **离线可用** | 断网也能改,回来能并 | 真实网络必然有断连 | **关键约束(不可逾越的边界):** - 🔴 **编辑天然并发**:同一位置可能多人同时改,且**各自的意图都要保留**。 - 🔴 **网络有延迟、会断连**:改动到达服务器和其他人的顺序不可预测。 - 🔴 **不能用粗暴的锁**:「锁住整段才能编辑」会彻底毁掉协作体验。 ## 4. 架构全景图 ``` 用户 A 浏览器 用户 B 浏览器 用户 C 浏览器 ┌────────────┐ ┌────────────┐ ┌────────────┐ │ 本地副本 + │ │ 本地副本 + │ │ 本地副本 + │ │ 编辑器 │ │ 编辑器 │ │ 编辑器 │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ 发送「操作(op)」 │ 双向、实时 │ └──────────WebSocket 长连接─────────┬──────────┘ ▼ ┌────────────────────────────────────────┐ │ 协同服务(合并引擎) │ │ • 同一文档的所有 op 路由到同一处理者 │ │ • 【单线程串行】处理,确定全局顺序 │ │ • OT / CRDT 合并:转换 op 使其互不覆盖 │ │ • 把合并后的 op 广播给所有协作者 │ └───────────────────┬────────────────────┘ ▼ ┌────────────────────────────────────────┐ │ 持久化:操作日志(追加)+ 定期快照 │ └────────────────────────────────────────┘ ``` > 灵魂部件是**协同服务里的合并引擎**:它把「多人乱序到达的编辑」整理成「一个所有人都认的顺序」,并保证合并后大家收敛到同一份。**注意它对每个文档是单线程串行的**——这是用「顺序」消灭「并发地狱」的关键一招。 ## 5. 组件职责 - **客户端本地副本 + 编辑器**:本地先改、立刻显示(乐观更新),同时把「操作」发往服务器。*为什么需要*:本地立即响应才有流畅手感;也是离线可用的基础。 - **长连接网关(WebSocket)**:维持每个协作者的双向实时通道。*为什么需要*:协同要服务器主动把别人的改动推下来,普通请求 - 响应做不到。 - **协同 / 合并引擎**:对每个文档**串行**处理所有操作,用 OT / CRDT 合并并发编辑,再广播。*为什么需要*:这是保证「不覆盖 + 收敛一致」的核心。 - **操作日志(追加)**:把每一个操作按最终顺序记下来。*为什么需要*:可回放重建文档、可做历史版本、可审计。 - **快照**:定期把文档完整状态存一份。*为什么需要*:避免每次都从头回放全部操作。 - **在线状态(presence)**:谁在线、光标在哪、选了什么。*为什么需要*:协作的「看见彼此」体验。 ## 6. 关键数据流 **场景一:两人并发编辑同一句话(协同的核心难题)** ``` 文档当前是:"你好世界" 用户 A 在位置 2 插入 "美丽的" → 想得到 "你好美丽的世界" 用户 B 同时删除位置 2 的 "好" → 想得到 "你世界" ✗ 覆盖式("最后写入者赢"):一个人的改动被丢掉 ✓ 合并引擎(OT):把后到的操作【转换】到「前一个操作已生效」的坐标系上, 使两个意图都保留 → 所有人最终都收敛到 "你美丽的世界" ``` **场景二:离线编辑后重连** ``` 1. 用户断网,本地继续改(操作攒在本地队列) 2. 重新联网 ──▶ 把离线期间的操作依次发给服务器 3. 服务器把它们和这期间别人的操作合并、排序 4. 把合并结果同步回来,本地副本收敛到与所有人一致 ``` ## 7. 数据模型与存储选择 核心思路:**文档 = 一串操作(op)按顺序作用的结果**,而不是一个被反复覆盖的「最终文本」。 | 数据 | 存储类型 | 为什么 | |---|---|---| | 操作日志(op 序列) | 追加型日志 | 只增不改,可回放、可做历史 | | 文档快照 | 文档型 / 对象存储 | 加速加载,免去回放全部操作 | | 文档元数据 / 权限 | 关系型 | 结构化、要一致 | | 在线状态(presence) | 内存 | 易失、高频、断线即失效 | > 教学点:**把状态表达为「操作序列」而非「最终值」**,一下子换来三样东西:协同合并、历史版本、完整审计。这正是事件溯源(Event Sourcing)思想——见 [04 · 架构模式](../../tutorial/04-十大核心架构模式.md)。 ## 8. 关键架构决策与权衡 ⭐ **决策 1:并发编辑怎么合并?锁 / OT / CRDT?(协同的命门)⭐** - **锁**:谁编辑谁锁住一段。实现最简单,但**本质是排队,不是协同**,体验灾难。 - **OT(操作转换)**:把后到的操作「转换」到前序操作生效后的坐标系上。**主流方案**,需要一个中心服务器来确定操作顺序,算法复杂但成熟。 - **CRDT(无冲突复制数据类型)**:把数据设计成「无论操作以什么顺序到达,合并结果都一致」。**天生适合离线和去中心**,但数据结构有额外的空间 / 元数据开销。 - **取向**:有中心服务器的产品多用 OT;强离线 / 端到端 / 去中心场景倾向 CRDT。**核心都是「保留意图,而非覆盖」。** **决策 2:同一文档的操作,单线程串行处理 ⭐** - 多节点并行处理同一文档的操作:会陷入「谁先谁后」的并发地狱。 - 把一个文档的所有操作路由到**同一个处理者、串行处理**:天然确定唯一顺序。 - **取向**:几乎必然选单 writer 串行。**这是「把并发问题转化为顺序问题」的经典手法**——单文档串行不会成为瓶颈,因为一份文档的编辑者数量有限。 **决策 3:同步整文档,还是只传增量操作?** - 传整文档:简单,但每次改一个字都传一大坨,带宽和延迟都差。 - 只传操作(op):「在位置 2 插入 X」就几个字节。 - **取向**:必传增量操作。这是实时协同流畅的基础。 **决策 4:只存快照,还是操作日志 + 快照?** - 只存最新快照:省事,但**没有历史、不能审计、无法回放**。 - 操作日志 + 定期快照:既能回溯任意版本,加载又快。 - **取向**:两者结合。日志给你「时间旅行」,快照给你「快速加载」。 ## 9. 规模化与瓶颈 - **第一个瓶颈:海量长连接**(每个协作者一条)。→ 破解:长连接接入层水平扩,按文档把连接归拢。 - **第二个瓶颈:超热文档**(几百人同时编辑同一份)。→ 破解:单文档单 writer 仍是上限,可对超大文档**分块**(不同章节独立协同)、或对只读者走广播而非参与合并。 - **第三个瓶颈:操作日志无限增长。** → 破解:定期打快照后,压缩 / 归档老操作。 - **按文档分片路由**:同一文档的所有操作必须落到同一处理者——这既是约束,也让分片变得清晰(按文档 ID 路由)。 ## 10. 安全与合规要点 - **权限粒度**:谁能查看 / 评论 / 编辑;权限可能在协作**进行中**被改变,要实时生效。 - **文档泄露**:共享链接的范围(任何人可见 vs 受邀)、过期、水印。 - **端到端加密的取舍**:E2EE 下服务器看不到明文,但**服务器看不到明文就难以在云端做合并** —— 这是隐私和协同能力之间的真实冲突。 - **审计**:谁在何时改了什么,操作日志天然支持。 ## 11. 常见误区 / 反模式 - ❌ **用「最后保存者覆盖」来处理多人编辑** → ✅ OT / CRDT 合并,保留所有人的意图。 - ❌ **用锁实现「协同」** → ✅ 那是排队;真协同要无锁合并。 - ❌ **同一文档的操作被多个节点乱序处理** → ✅ 单文档单 writer 串行,确定唯一顺序。 - ❌ **每次改动都同步整个文档** → ✅ 只传增量操作。 - ❌ **只存最新快照,丢掉操作历史** → ✅ 操作日志 + 快照,换来版本与审计。 ## 12. 演进路线:MVP → 成长期 → 成熟期 | 阶段 | 规模量级 | 架构长什么样 | 此时该操心什么 | |---|---|---|---| | **MVP** | 单文档少数人 | 中心服务器 OT + WebSocket + 操作日志 | 先把「两人同时编辑不打架」跑通 | | **成长期** | 多团队 | 按文档分片、presence、离线同步、历史版本、权限 | 实时性、长连接规模、冲突体验 | | **成熟期** | 大规模 / 跨区域 | CRDT 或强化 OT、超大文档分块协同、富内容、跨区域低延迟 | 规模、延迟、端到端安全、富表达 | ## 13. 可复用要点 - 💡 **「单 writer 串行化」是消灭并发复杂度的利器**:把「多个东西同时改」的难题,转化为「按一个确定顺序依次处理」。 - 💡 **协同的精髓是「保留意图,而非覆盖结果」。** 任何多方修改同一资源的系统(包括 [云存储](../cloud-storage/README.md) 的同步冲突)都该这么想。 - 💡 **把状态建模为「操作序列」而非「最终值」**,一举换来合并能力、历史版本和审计——这就是事件溯源。 - 💡 **操作日志 + 定期快照** 是「既要完整历史、又要快速加载」的通用组合拳。 ## 🎯 随堂检验 --- ## 参考原型与延伸阅读 > 本模板基于以下**真实开源项目**与**工程博客**整理。 **🔧 开源原型(可直接读代码):** - [yjs/yjs](https://github.com/yjs/yjs) — 高性能 CRDT,提供可自动合并的共享数据类型,支持离线编辑、快照、共享光标。 **📖 工程博客:** - [How Figma's multiplayer technology works (Figma)](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/) — 为何弃用纯 OT、采用类 CRDT 的客户端 / 服务端 + WebSocket 实时同步。 - [I was wrong. CRDTs are the future (Joseph Gentle)](https://josephg.com/blog/crdts-are-the-future/) — ShareDB / OT 资深作者反思 OT vs CRDT 取舍的经典长文。 --- > 📌 一句话记住实时协同文档:**它不是「一个能多人打开的文档」,而是「一台在没有锁的情况下,把所有人乱序的编辑意图合并成单一一致状态的引擎」——所有设计都在回答『两个人同时改同一处,怎么谁都不丢、最后还一致』。**