Skip to content

实时协同文档 架构模板

代表产品: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 · 架构模式

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 串行化」是消灭并发复杂度的利器:把「多个东西同时改」的难题,转化为「按一个确定顺序依次处理」。
  • 💡 协同的精髓是「保留意图,而非覆盖结果」。 任何多方修改同一资源的系统(包括 云存储 的同步冲突)都该这么想。
  • 💡 把状态建模为「操作序列」而非「最终值」,一举换来合并能力、历史版本和审计——这就是事件溯源。
  • 💡 操作日志 + 定期快照 是「既要完整历史、又要快速加载」的通用组合拳。

🎯 随堂检验

🤔多人同时编辑同一处内容,正确的处理方式是?
  • A最后保存者覆盖前面的
  • B用 OT / CRDT 合并,保留所有人的编辑意图
  • C加锁,谁先抢到谁改,其他人等

参考原型与延伸阅读

本模板基于以下真实开源项目工程博客整理。

🔧 开源原型(可直接读代码):

  • yjs/yjs — 高性能 CRDT,提供可自动合并的共享数据类型,支持离线编辑、快照、共享光标。

📖 工程博客:


📌 一句话记住实时协同文档:它不是「一个能多人打开的文档」,而是「一台在没有锁的情况下,把所有人乱序的编辑意图合并成单一一致状态的引擎」——所有设计都在回答『两个人同时改同一处,怎么谁都不丢、最后还一致』。