# 案例 04 · SyncRoom:给远程团队用的实时协同工作台 > 一句话点题:**本案例练的是「实时协同里的顺序与收敛」——协同系统的难点不是把消息发出去,而是在断线、重连、多端、并发编辑、重复投递下,让每个人最终看到同一段可信的协作历史。** --- > **🧪 案例篇第 4 篇 · 本案例只练一件事** > > 练**实时聊天 + 协同文档 + 通知推送**下的架构判断:什么时候简单轮询够用,什么时候必须上长连接,消息顺序到底谁说了算,离线用户怎么补齐,多人同时编辑怎么不互相覆盖,以及哪些状态可以最终一致。 > > | 读完你应该能 | 本案例靠什么练 | > |---|---| > | 说清实时系统为什么不能只靠请求响应 | 用 WebSocket 长连接、连接路由和心跳解释实时投递 | > | 判断消息顺序、重复、离线补齐怎么兜住 | 用服务端 seq、ack、重试、幂等去重和断点拉取 | > | 看懂协同编辑为什么不能最后保存覆盖 | 用操作日志、单文档串行、OT / CRDT 解释冲突合并 | > | 区分强一致数据和可松弛状态 | 把消息 / 文档当核心状态,把在线、正在输入、已读、通知当旁路状态 | > > **重要提醒:下面是教学化案例,不是某个协同产品的内部图纸。** 数字用于数量级推理,目的是练判断,不是给出唯一答案。 --- ## 开场:为什么「能发消息」不等于实时协同 因为远程团队真正需要的,不是一个会刷新的聊天框,而是一个让大家觉得「我们正在同一个房间里工作」的系统。 > **SyncRoom** 是一个给远程团队用的实时协同工作台。一个项目房间里,成员可以聊天、@ 同事、共同编辑会议纪要、看到谁在线、收到离线推送。有人断网后回来,不能丢消息;两个人同时改同一段纪要,不能互相覆盖;手机、网页、桌面端要看到一致的未读和历史。 第一眼看,它像是几个普通功能拼在一起: - 房间聊天; - 在线状态; - 正在输入; - 已读 / 未读; - 会议纪要协同编辑; - @ 提醒和离线推送。 但真正上线后,最容易出事故的不是「消息晚了 1 秒」,而是: > **消息乱序、重复、漏收;离线回来补不齐;多人同时编辑互相覆盖;未读数和已读状态在多端之间打架。** 所以这一章不写「怎么做一个 WebSocket Demo」。它问一个更具体的问题: > **如何在不可靠网络上,让聊天消息可靠有序,让协同文档最终一致,同时让在线状态和通知保持克制?** 这章和前三章的压力源不同: - [StarArena](../stararena-ticketing/README.md) 怕的是高并发和库存错误。 - [PatchDesk](../patchdesk-saas/README.md) 怕的是多租户边界和副作用失控。 - [DocuMind](../documind-rag/README.md) 怕的是答案不可信和证据链断裂。 - **SyncRoom 怕的是实时网络不可靠,但用户仍然期待所有人看到同一段协作历史。** --- ## 读前小词典 这篇会反复出现几个词,先用人话对齐一下: | 词 | 人话解释 | |---|---| | WebSocket | 浏览器和服务器之间保持不断开的双向连接,服务器可以主动推消息给客户端。 | | SSE | Server-Sent Events,服务器向浏览器单向推送事件。适合只下行推送,不适合复杂双向协作。 | | 长连接 | 连接建立后长期保持,不像普通 HTTP 请求那样用完就断。实时系统通常靠它做低延迟推送。 | | 心跳 | 客户端和服务器定期互相报平安。长时间没心跳,就认为连接断了。 | | 路由表 | 记录「用户现在连在哪台网关」。要推消息给某人,先查他在哪。 | | seq | sequence number,序号。服务端给同一房间 / 会话里的消息分配递增序号,客户端按它排序。 | | ack | acknowledgement,确认。客户端告诉服务端「这条我收到了」。 | | 幂等 | 同一条消息重复处理多次,最终只产生一次效果。重试一定会带来重复,所以要幂等。 | | 离线补齐 | 用户断网或离线后,回来按上次收到的位置继续拉取缺失消息。 | | 位点 / cursor | 客户端记录「我已经收到 / 读到哪里了」的位置。 | | read watermark | 已读水位。可以理解成「这个用户在这个房间已读到哪条 seq」。未读数可以用它重新算。 | | Presence | 在线状态、正在输入、光标位置这类临场感状态。它们很有用,但通常不需要强一致。 | | OT | Operational Transformation,操作转换。把并发编辑操作转换到同一个顺序下,避免互相覆盖。 | | CRDT | Conflict-free Replicated Data Type,无冲突复制数据类型。让不同端的并发修改最终自动合并到一致。 | | 操作日志 | 把每次编辑记录成一条 op,而不是只保存最终文本。它能回放、审计、做版本历史。 | | 快照 | 定期保存文档当前完整状态,避免每次打开都从第一条操作回放。 | --- ## 一、起始状态:先把实时基本盘做对 SyncRoom 的第一版目标很朴素:**让一个 20 人以内团队在项目房间里聊天、看消息历史、共同维护一份会议纪要。** 起始阶段的约束大概是这样: | 维度 | 起始阶段 | |---|---| | 团队规模 | 5~8 名工程师 | | 房间数 | 1,000 个以内 | | 单房间成员 | 5~30 人 | | 同时在线用户 | 1,000~5,000 | | 峰值消息写入 | 50~200 条/秒 | | 协同文档 | 每个房间 1~3 份纪要 | | 核心目标 | 聊天不丢不乱序,文档不互相覆盖 | | 最不能错 | 用户发过的消息丢了;两个人同时编辑导致一方内容消失 | 这时最合理的架构不是一上来多区域、多活、复杂 CRDT 平台,而是**中心化长连接网关 + 消息服务 + 操作日志 + 简单协同引擎**: ``` 浏览器 / 移动端 │ WebSocket ▼ ┌────────────────────────────────────────────┐ │ 长连接网关 │ │ 心跳、连接管理、上行消息、下行推送 │ └──────────────┬─────────────────────────────┘ ▼ ┌────────────────────────────────────────────┐ │ SyncRoom 后端 │ │ 房间消息 → 分配 seq → 落库 → 投递 / 离线补齐 │ │ 协同文档 → op 合并 → 操作日志 → 广播 │ │ Presence / 通知 → 旁路异步 │ └──────────────┬─────────────────────────────┘ ▼ ┌──────────────┐ │ 消息 / op 存储 │ │ 快照 / 位点 │ └──────────────┘ ``` 这不是「先堆复杂度」。它只是把实时协同最基本的底线立住:**核心历史要可靠,临场状态可以松弛。** --- ## 二、量化假设:它不是被大请求压垮,而是被长连接和乱序压垮 先算一笔账。假设 SyncRoom 推广到中型远程团队市场半年后: ``` 注册团队:2,000 活跃团队:500 日活用户:30,000 同时在线用户:8,000~20,000 人均设备:1.5~2.2 台 峰值长连接:15,000~40,000 条 房间总数:50,000 活跃房间:5,000 峰值聊天消息:1,000~3,000 条/秒 峰值 presence 事件:10,000~50,000 条/秒 协同文档操作:500~2,000 op/秒 单房间常见在线人数:5~50 大房间在线人数:200~1,000 目标:在线消息 P95 < 300ms,离线补齐 P95 < 2s 协同目标:本地编辑立即显示,远端同步 P95 < 500ms Presence 目标:允许 5~15 秒内最终收敛 可靠性目标:核心消息不丢;重复消息不重复展示;同房间消息按服务端 seq 展示 ``` 这个系统的单条消息很小,但负载形态很特殊: 1. **连接是有状态的**:用户连在哪台网关,消息就必须推到哪台网关。 2. **网络一定会断**:移动端切后台、地铁弱网、电脑睡眠,都会让连接消失。 3. **到达顺序不等于真实顺序**:消息和编辑操作会因为网络抖动乱序到达。 4. **状态更新极多**:在线、正在输入、光标、已读变化比正式消息多得多。 所以 SyncRoom 的架构重心不是「怎么处理一个大请求」,而是: > **把核心协作历史做成可靠有序,把易变临场状态做成可降级的最终一致。** --- ## 三、触发信号:什么时候说明第一版开始不够用 第一版跑起来后,不要凭感觉升级。看这些信号: | 信号 | 表现 | 为什么这是架构问题 | |---|---|---| | 消息偶发乱序 | 「收到」出现在「你能看见吗」之前 | 客户端按到达时间排序,没有服务端 seq | | 用户说消息丢了 | 断网重连后少了几条中间消息 | 没有持久化位点和离线补齐 | | 消息重复展示 | 弱网下同一条消息出现两次 | 重试没有幂等去重 | | 多端未读不一致 | 手机显示 3 条未读,网页显示 0 条 | 已读位点没有服务端统一收敛 | | 大房间卡顿 | 500 人房间里 presence 事件把网关打满 | 临场状态没有限频 / 聚合 / 降级 | | 会议纪要被覆盖 | A 写的段落被 B 后保存的版本覆盖 | 把协同文档当普通表单保存,没有 op 合并 | | 重连后文档跳变 | 离线编辑回来后,本地内容被服务端快照覆盖 | 没有 OT / CRDT 合并离线操作 | | @ 通知轰炸 | 一次讨论触发多端、多渠道重复提醒 | 通知没有去重、限频、在线 / 离线分流 | 这些信号不是在说「加机器就行」。它们在说:**实时系统缺少可靠投递、顺序和合并协议。** --- ## 四、核心矛盾:用户要实时感,系统要可信历史 SyncRoom 的核心对象有三组: 1. **房间 / 成员 / 连接**:谁在房间里,现在连在哪台网关。 2. **消息 / 位点 / 通知**:发了什么,谁收到哪里,谁读到哪里,谁需要提醒。 3. **文档 / 操作 / 快照**:编辑意图是什么,如何合并,如何重建最终文档。 如果只看最简单路径,它像这样: ``` 用户发消息 → 服务器转发 → 其他人显示 用户改文档 → 保存最新文本 → 其他人刷新 ``` 但真实系统必须在每一步都回答: - 这条消息的服务端顺序是多少? - 收方在线吗?连在哪台网关? - 断线重连后从哪个位点补齐? - 这条重试消息是不是已经处理过? - 多端已读状态以谁为准? - 两个人同时改同一段文字,谁的意图应该保留? - 在线状态和正在输入能不能丢?能不能延迟? 所以新的架构命题变成: > **核心历史必须可靠有序;协同编辑必须合并意图;临场状态和通知必须克制降级。** 这里最容易混淆的是:聊天消息和协同文档不是同一种一致性问题。 ``` 聊天消息:追加历史 → 重点是服务端顺序、投递、补齐、去重 协同文档:多人改同一状态 → 重点是操作合并、收敛一致、不覆盖 Presence:临场感状态 → 重点是快、轻、可过期、可降级 ``` 把这三类状态混成一套强一致模型,会又慢又贵;把三类状态都当临时消息,又会丢历史、乱顺序、覆盖编辑。 --- ## 五、方案推演:实时协同到底靠什么兜住 这是本案例最重要的决策。很多实时 Demo 能跑,但一到弱网、多端、离线和并发编辑就崩。 ### 方案 A:轮询 + 最后保存者覆盖 ``` 客户端每 3 秒拉一次新消息 文档每次保存整篇文本 最后保存的人覆盖前一个版本 ``` | 优点 | 代价 | |---|---| | 实现最简单,普通 Web 栈就能做 | 不够实时,服务端被轮询浪费打满 | | 文档保存逻辑简单 | 并发编辑会丢内容,离线合并基本不可控 | ### 方案 B:WebSocket 推消息,但顺序靠客户端 ``` 客户端发消息 → 网关广播 → 客户端按本地时间 / 到达时间展示 ``` | 优点 | 代价 | |---|---| | 实时感明显提升 | 网络抖动会乱序,重试会重复 | | 适合小房间 Demo | 断线补齐、多端同步、未读位点没有可靠基础 | ### 方案 C:服务端 seq + 持久化 + ack / 重试 / 去重 ``` 发送消息 └─▶ 服务端分配 room_seq └─▶ 先持久化 └─▶ 在线投递 / 离线存位点 └─▶ 客户端 ack,按 seq 去重和排序 ``` | 优点 | 代价 | |---|---| | 同房间消息有服务端统一顺序 | 要维护发号、ack、重试和位点 | | 离线重连可按 last_seen_seq 补齐 | 协议复杂度明显上升 | | 重复投递不会重复展示 | 客户端和服务端都要存消息 ID / seq | ### 方案 D:协同文档用操作日志 + OT / CRDT ``` 用户编辑不是保存整篇文本 而是发送操作:在位置 10 插入「结论」 服务端 / CRDT 引擎合并操作 所有端最终收敛到同一份文档 ``` | 优点 | 代价 | |---|---| | 不会因为最后保存覆盖别人 | OT / CRDT 理解和实现成本高 | | 支持离线编辑后合并 | 需要操作日志、快照、冲突测试 | | 能做历史版本和审计 | 文档模型要从「最终文本」变成「操作序列」 | SyncRoom 第一阶段选择:**聊天用服务端 seq + ack / 重试 / 去重;文档用中心化 OT 或成熟 CRDT 库;presence 和通知走最终一致旁路。** 关键不在「有没有 WebSocket」,而在于: > **实时只是体验,顺序、补齐、合并、幂等才是可信协作的底座。** --- ## 六、关键架构决策:用 ADR 把为什么留下来 ADR 是 Architecture Decision Record,可以理解成「架构决策记录」。实时系统最容易在后面被人问:「为什么不按客户端时间排序?为什么要先落库再投递?为什么文档不直接保存全文?为什么在线状态可以不准?」这些都应该提前写下来。 ### ADR-01:采用 WebSocket 长连接网关 + 用户连接路由表,SSE / HTTP 作为降级 - **背景**:实时体验要求服务端能主动推送;普通 HTTP 请求响应无法低延迟通知在线用户。 - **选择**:客户端通过 WebSocket 接入长连接网关;连接建立 / 断开时更新 `user_id -> gateway_id` 路由表;投递消息前先查路由。企业代理、弱网或只读旁观场景允许 SSE 下行 + HTTP 上行降级。 - **放弃**:放弃用短轮询支撑核心实时体验。 - **换来**:在线消息低延迟,服务端能精准找到用户当前连接;只读场景仍有可用降级路径。 - **风险**:长连接是有状态的,网关故障会导致其上用户同时重连,路由表也会高频变化。 - **复审条件**:当连接数超过单集群承载或跨地域延迟明显时,再做就近接入、多区域网关和连接迁移;如果只读旁观远多于编辑,可以扩大 SSE 使用范围。 ### ADR-02:聊天消息由服务端分配房间内 seq,先持久化再投递 - **背景**:客户端时间不可信,网络到达顺序不可信,投递也可能重复。 - **选择**:每个房间的消息由服务端分配递增 `room_seq`;消息先写入持久存储,再在线投递或离线补齐;客户端按 `room_seq` 排序,按 `message_id` 去重。 - **放弃**:放弃按客户端时间 / 到达时间展示消息。 - **换来**:同房间历史有统一顺序,断线后可从 `last_seen_seq` 补齐,重复投递不会重复展示。 - **风险**:热点大房间的 seq 分配和写入可能成为瓶颈。 - **复审条件**:当单房间写入过高时,考虑房间分片、频道拆分或大房间共享流 + 位点读取。 ### ADR-03:协同文档保存操作日志 + 快照,不用最后保存覆盖 - **背景**:多人会同时编辑同一份纪要,保存最终文本会丢掉并发意图。 - **选择**:客户端发送编辑操作 op;同一文档路由到同一合并处理者串行处理;服务端为 op 分配 `doc_seq` / 文档版本;用 OT 或 CRDT 合并操作;持久化操作日志,定期生成快照。 - **放弃**:放弃「每次保存整篇文档,最后写入者获胜」。 - **换来**:并发编辑不互相覆盖,离线操作能按 `last_doc_seq` 补齐和合并,历史版本和审计天然存在。 - **风险**:OT / CRDT 边界复杂,富文本、表格、图片等结构会放大算法难度。 - **复审条件**:如果离线编辑占比高、跨端并发复杂,优先考虑成熟 CRDT 库;如果中心化协作足够,OT + 单文档单 writer 更容易控制。 ### ADR-04:presence、正在输入、已读和通知走旁路,不压住核心链路 - **背景**:在线状态、正在输入、光标、已读和推送提醒变化频繁,但允许短暂不准。 - **选择**:presence 放带 TTL 的高速存储;正在输入限频广播;已读以服务端位点收敛;通知异步入队,在线时尽量站内 / 长连接提醒,离线时走 Push / 邮件。 - **放弃**:放弃把所有临场状态都做成强一致事务。 - **换来**:核心消息和文档链路不被高频状态拖慢,状态可降级,通知不轰炸。 - **风险**:用户会短暂看到不准确的在线 / 已读状态。 - **复审条件**:如果已读状态成为合规或业务承诺,再提高它的持久化与确认级别;presence 仍不应强一致化。 --- ## 七、演进后的结构与数据流 下面只画 SyncRoom 的核心结构。它不是一个 WebSocket 接口,而是一套消息、文档、临场状态分层处理的协作系统。 ### 起始路径 ``` 用户发消息 └─▶ 网关广播 └─▶ 客户端显示 用户改纪要 └─▶ 保存整篇文本 └─▶ 覆盖旧版本 ``` 问题是:消息没有统一顺序,断线没有补齐基础,协同文档会被最后保存覆盖。 ### 演进后的结构 ``` 客户端 Web / Mobile / Desktop │ WebSocket ▼ ┌──────────────────────────────────────────────┐ │ 长连接网关层 │ │ 心跳、连接管理、上行转发、下行推送 │ └───────────────┬──────────────────────────────┘ ▼ ┌──────────────────────────────────────────────┐ │ SyncRoom 核心服务 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 消息服务 │ │ 协同文档服务 │ │ │ │ room_seq/ack │ │ op merge │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ ┌──────▼───────┐ ┌──────▼───────┐ │ │ │ 消息存储 │ │ op 日志 + 快照 │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 路由表 │ │ Presence/通知 │ │ │ │ user->gateway│ │ TTL/队列/限频 │ │ │ └──────────────┘ └──────────────┘ │ └──────────────────────────────────────────────┘ ``` 这张图的核心变化不是「用了 WebSocket」,而是结构变清楚了: - **长连接网关**只负责连接和转发,不决定业务顺序。 - **消息服务**分配房间内 seq,先落库再投递,用 ack / 重试 / 去重兜可靠性。 - **协同文档服务**处理编辑操作,用 OT / CRDT 合并,并保存操作日志和快照。 - **路由表**解决「用户现在连在哪台网关」。 - **Presence / 通知**走旁路,可限频、可过期、可降级。 ### 跟一次「断线后重连补齐」走到底 ``` 1. 用户 A 在房间 R 里发消息。 2. 消息服务给它分配 room_seq=1042,写入消息存储。 3. 用户 B 此时在线,路由表显示 B 连在 gateway-7,消息被推过去。 4. B 的网络抖动,没有及时 ack。 5. 服务端重试投递;B 客户端用 message_id / room_seq 去重,只展示一次。 6. B 断线 2 分钟,期间房间又产生 seq=1043~1050。 7. B 重连时带上 last_seen_seq=1042。 8. 服务端返回 seq > 1042 的消息,客户端按 seq 排序补齐。 9. 未读位点更新到服务端,手机和网页最终收敛到同一未读状态。 ``` 这里的关键点: - 消息顺序由服务端 seq 决定,不是客户端时间。 - 投递可以重复,展示必须幂等。 - 离线补齐靠位点,不是靠「推送一定成功」。 - 未读和已读最终以服务端位点收敛,不要让每个端各算各的。 ### 跟一次「两人同时改会议纪要」走到底 ``` 1. 文档当前版本是 v10。 2. 用户 A 在标题下插入「结论:继续推进」,生成 opA。 3. 用户 B 同时删除同一段旧结论,生成 opB。 4. 两个 op 通过 WebSocket 到达协同文档服务,到达顺序可能不同。 5. 同一文档的 op 被路由到同一处理者串行处理。 6. 服务端为合并后的 op 分配 doc_seq,OT / CRDT 引擎保留两人的编辑意图。 7. 合并后的 op 写入操作日志,更新当前版本 v11。 8. 服务端把最终 op 广播给所有协作者。 9. 每个客户端应用同一批 op,最终看到同一份纪要。 10. 定期快照保存 v11 的完整文档,下次打开不用从第一条 op 回放。 ``` 这里的关键点: - 协同编辑保存的是操作,不是只保存最终文本。 - 同一文档要有一个确定顺序,否则并发操作无法稳定合并。 - 客户端重连时也要带 `last_doc_seq`,用来补齐断线期间缺失的文档操作。 - OT / CRDT 的目标不是谁赢,而是保留意图并最终收敛。 - 操作日志既是合并基础,也是历史版本和审计基础。 --- ## 八、坏了怎么办:故障场景与兜底 | 故障 | 直接后果 | 检测方式 | 架构兜底 | |---|---|---|---| | 网关宕机 | 这台网关上的用户全部掉线 | 连接数突降、重连峰值 | 客户端指数退避重连;路由表过期;网关水平扩展 | | 路由表过期 | 消息投到错误网关或投不出去 | 投递失败率、路由 miss | 路由带 TTL;投递失败后查最新连接;客户端重连刷新 | | 消息未先落库就推送 | 推送成功但服务端历史缺失,或服务故障后丢消息 | 消息 ID 对账、用户投诉 | 先持久化再投递;未落库消息不允许 ack 成功 | | 按客户端时间排序 | 弱网下消息乱序 | 乱序率、客户端日志 | 服务端分配 room_seq;客户端按 seq 展示 | | 重试无去重 | 同一消息重复出现 | duplicate message_id | 客户端和服务端按 message_id / seq 幂等 | | 离线补齐只靠 Push | 用户点开后历史仍缺消息 | last_seen_seq 缺口 | Push 只负责唤醒;上线后按位点拉取缺失消息 | | 未读由各端本地计算 | 手机、网页、桌面未读数不一致 | 多端状态差异 | 服务端保存 read watermark / read_cursor,各端最终收敛 | | 未读计数只做缓存增减 | 漏减或重复减后长期错误 | 未读数和 seq 差异 | 计数可缓存,但要能用 read watermark + 消息 seq 重算 | | 大房间 presence 全量广播 | 网关被在线 / 光标 / 正在输入事件打满 | presence QPS、广播量 | 限频、采样、聚合;只发给正在看房间的人 | | 最后保存者覆盖文档 | 并发编辑丢内容 | 文档 diff、用户反馈 | 操作日志 + OT / CRDT;禁止整篇覆盖式保存 | | 协同 op 重放失败 | 打开文档状态不一致 | 快照校验、op 回放测试 | 操作日志不可变;定期快照;坏 op 隔离和修复 | | CRDT / OT 冲突边界没测 | 富文本、表格、图片编辑出现错位 | 协同 fuzz test、回放测试 | 建立并发编辑测试集;复杂结构分块协同 | | SSE 被当成万能双向通道 | 上行仍靠 HTTP,协同编辑体验受限 | 降级链路延迟、重试量 | SSE 适合作只读 / 降级,主编辑通道仍用 WebSocket | | 通知重复轰炸 | @ 提醒在多端、多渠道重复到达 | 通知去重率、退订率 | 通知异步队列;按事件 ID 去重;在线优先站内,离线再 Push | 实时协同的成熟度,不是看 Demo 里消息多快飞过去,而是看弱网、重连、重复、并发编辑这些坏情况有没有协议兜住。 --- ## 📌 拿模板验证这次推演 本案例不是重写实时聊天或协同文档模板,而是把「远程团队协作」里最容易混在一起的几类状态拆开推演。 | 可复用模板 / 章节 | 本案例复用什么 | 本案例重点补什么 | |---|---|---| | [实时通讯](../../templates/realtime-chat/README.md) | 长连接、路由表、服务端序号、ack / 重试 / 去重、离线补齐 | 把聊天消息放进团队房间和多端未读场景 | | [实时协同文档](../../templates/collaborative-doc/README.md) | 操作日志、单文档串行、OT / CRDT、快照 | 把协同编辑和聊天、presence、通知放在同一产品里取舍 | | [通知 / 推送系统](../../templates/notification-system/README.md) | 异步通知、去重、限频、多渠道投递 | 说明在线走长连接,离线才需要 Push / 邮件唤醒 | | [分布式系统的硬道理](../../tutorial/10-分布式系统的硬道理.md) | 网络不可靠、部分失败、重试 | 把断线、重连、重复投递作为默认情况 | | [数据一致性工程](../../tutorial/11-数据一致性工程.md) | 幂等、重试、最终一致、补偿 | 把消息投递和已读位点做成可恢复协议 | | [为失败而设计](../../tutorial/12-为失败而设计.md) | 降级、隔离、熔断 | presence 和通知可降级,不要拖垮核心消息与文档 | > **读法建议**:先读本章,再回看 [实时通讯模板](../../templates/realtime-chat/README.md) 和 [实时协同文档模板](../../templates/collaborative-doc/README.md)。你会更容易看懂:聊天和协同文档都实时,但它们要解决的可靠性问题不是同一个。 --- ## 🎯 随堂检验 --- ## 本案例小结 - **实时不是核心,可信协作历史才是核心。** WebSocket 只是通道;服务端 seq、落库、ack、补齐、去重才让聊天可靠。 - **聊天和协同文档不是同一种问题。** 聊天是追加历史,重点是有序投递;文档是多人修改同一状态,重点是 OT / CRDT 合并和收敛。 - **离线补齐不能靠 Push。** Push 只负责唤醒,真正补齐要靠服务端消息历史和客户端位点。 - **多端状态要有服务端收敛点。** 未读 / 已读不能各端各算,否则永远打架。 - **Presence 可以松弛。** 在线、正在输入、光标位置可以短暂不准,需要限频和降级,不要和核心消息链路抢资源。 - **操作日志比最终文本更适合协同。** 它支持合并、回放、版本历史和审计,是协同编辑的地基。 > **承上启下**:这一章把 [实时通讯](../../templates/realtime-chat/README.md)、[实时协同文档](../../templates/collaborative-doc/README.md) 和 [通知系统](../../templates/notification-system/README.md) 放进同一个产品里。下一章如果继续写内容 Feed / 视频分发,压力会再次变化:重点会从实时收敛,转向扇出放大、热点内容、推荐、搜索和 CDN 分发。 --- ## 相关链接 - 模板对照:[实时通讯](../../templates/realtime-chat/README.md) · [实时协同文档](../../templates/collaborative-doc/README.md) · [通知 / 推送系统](../../templates/notification-system/README.md) - 方法论:[02 · 架构师的思考框架](../../tutorial/02-架构师的思考框架.md) · [07 · 从 0 到 1 设计一个系统](../../tutorial/07-从0到1设计一个系统.md) · [08 · 架构决策记录与演进](../../tutorial/08-架构决策记录与演进.md) - 硬骨头:[10 · 分布式系统的硬道理](../../tutorial/10-分布式系统的硬道理.md) · [11 · 数据一致性工程](../../tutorial/11-数据一致性工程.md) · [12 · 为失败而设计](../../tutorial/12-为失败而设计.md)