1.1 需求拆解:实时、多人、可回放
协作白板的难点,常被一句「画笔画线」盖住,但拆成工程语言就是三条互相牵制的需求:实时、多人、可回放。它们决定了后面所有模块的边界,先拆清楚能少走很多弯路。很多团队第一周就栽在这:把白板当「前端画布 + 一个定时器」,结果上线才发现实时和多人根本没解,只能推倒重来。本篇把这三条逐条拆到模块级,给后面选型定基调,并附上一组「不满足会出什么事故」的对照表,方便对照自查。拆需求最怕的不是漏一条,而是三条都没拆到模块级——口头说「要实时」,落到代码就变成「加个定时器」,结果上线一压就散。
三条需求各自牵动什么
| 需求 | 表面含义 | 真正牵动的模块 | 不满足会怎样 |
|---|---|---|---|
| 实时 | 我画一笔,别人立刻看到 | 长连接、服务端中继、节流 | 延迟肉眼可见,协作变「轮流画」 |
| 多人 | 同一房间同时在线 | 房间模型、广播扇出、冲突合并 | 千人房间直接卡死或数据错乱 |
| 可回放 | 录下操作流,能倒着退 | 操作日志、时间戳、撤销栈 | 撤销等于重画,历史无法分享 |
三条里「实时」最容易误解,「多人」最容易被低估,「可回放」最常被当成锦上添花。实时常被误读成「画得快」,多人常被误读成「能同时画」,可回放常被当成「录个屏就行」。下一节先把「实时到底要什么」讲透,因为它直接决定后面协议怎么设计,也决定了整条链路的指标口径。
实时不是「画得秒」而是「状态秒」
新手的误区是把实时理解成「我这条线画得够快」,其实协作要的是对端能尽快收敛到同一块板。一笔笔触要拆成多个坐标点,每点带时间戳和序号;对端不是把这条线再画一遍,而是按时间顺序把增量合并进本地画布。这决定了后面必须做增量协议、顺序编号、并发合并三件套。 | 常见误解 | 真正要做 | |---|---| | 把「传得快」当实时 | 把「对端收敛得快」当实时 | | 一条线整体传 | 拆成坐标点按序合并 | | 只保证带宽 | 还要保证顺序与幂等 |
实时性最终落到「收敛速度」上,这个口径能指导后面所有指标怎么定,别再用「画得快」当 KPI。再进一步说:收敛不只是「快」,还要「稳」——同样的操作序列,任何时刻重放都得收敛到同一块板,这就是「确定性」,是回放与撤销的地基。快是体验指标,稳是正确性指标,两者缺一不可:只快不稳,多人同画就画面分叉;只稳不快,体验又退回「轮流画」。
多人意味着「扇出 + 合并」两笔账
一个房间里我发一笔,服务端要推给其余 N-1 人,这是广播扇出;两人几乎同时画,事件到达顺序会乱,必须有冲突合并规则(OT 或 CRDT,见 3.3)。扇出决定带宽和延迟,合并决定正确性,两者要一起设计,不能只保证快。 | 账 | 牵动 | 代价 | 量级示例 | |---|---|---|---| | 扇出 | 带宽、延迟 | 千人房间 O(N) 推,笔越多越贵 | 1000 人 × 100 笔/秒 = 10 万事件/秒 | | 合并 | 正确性 | 规则不统一就画面分叉、闪烁 | 冲突窗口 50ms 内 |
扇出是「贵」的账,合并是「对」的账,两边都要算。Go 侧做服务治理时同样会撞「多个调用方同时改一份共享态」的问题,拆解思路可对照 Go 微服务实战 里关于共享状态与事件顺序的章节。再把这笔账摊到产品侧:房间越大、笔频越高,扇出成本呈平方级增长,这正是 4.4 节「批量 + 节流」要压的核心指标。很多人只做扇出不做合并,结果房间一热,「贵」压垮了服务;或者只做合并不做扇出优化,结果「对」保证了但「快」没保证,体验照样崩。
可回放 = 日志驱动,不是截图驱动
回放若靠「定时截图」只能看静态快照;靠「操作日志」才能像录像一样倒放、撤销。所以白板要维护一条只增不改的事件流:每笔、每次撤销、每次房间变更都入流,回放就是按日志重放,撤销就是重放到某个时间戳再反向应用。 | 驱动方式 | 能做什么 | 做不了什么 | 适合场景 | |---|---|---|---| | 截图驱动 | 看某时刻静态 | 不能倒放、不能并 | 存档、封面 | | 日志驱动 | 倒放、撤销、分享 | 要维护有序事件流 | 协作回放 | | 混合 | 锚点 + 增量 | 复杂度更高 | 长期存档 |
这条流同时是持久化和分享的基础。Rust 侧做日志管道时对「只增流」的建模与削峰,可以参考 Rust 实战 里高吞吐日志流的章节,二者都是「只增 + 可重放」的范式。截图驱动省内存但丢历史,日志驱动保历史但涨存储,混合驱动用锚点加增量做折中——选哪种取决于「回放要倒多远、存档要存多久」。
断线重连是被漏掉的第 N+1 条
拆需求时别把「断线重连」漏掉。它不算上面三条,但缺了它,多人和可回放都会塌:重连后要补齐断线期间错过的增量,且补齐本身也可能乱序。本篇先点出,下一节协议设计会专门留位置。再提醒一点:「各画各的」并不天然无冲突,图层顺序、擦除掩膜是全局的,漏了它就会出现「同区两笔反复闪烁」,这要交给 3.3 的合并规则。断线重连之所以常被漏,是因为它藏在「多人」里——只要多人就必有断线,缺了它多人和可回放一起塌。
需求拆解的自查清单
| 自查项 | 不满足的信号 | 对应章节 |
|---|---|---|
| 收敛速度 | 两人同画画面不同步 | 3.3 |
| 扇出可控 | 热房间带宽爆 | 4.4 |
| 顺序可追溯 | 乱序、重复事件 | 2.3 |
| 可回放 | 撤销等于重画 | 3.4 |
| 断线可补 | 重连后画面错 | 2.4 |
把这张表作为需求拆解的「验收线」,后面选型、协议、持久化都往这里对齐。每拆完一条需求,回头对一遍这张表,缺哪条补哪条。
三条需求的优先级与节奏
| 优先级 | 需求 | 理由 | 上线节奏 |
|---|---|---|---|
| P0 | 实时 | 协作的核心价值 | 第一版必须 |
| P0 | 多人 | 产品定位 | 第一版必须 |
| P1 | 可回放 | 可交付价值 | 第二版补齐 |
| P1 | 断线重连 | 多人场景兜底 | 随多人上线 |
优先级定了,资源就往 P0 倾斜,P1 在 P0 稳定后补齐,别一开始就四条齐平铺。把「为什么先做这条」写进需求文档,后面选型、排期才有依据。
拆需求时最容易被带偏的三处
| 误区 | 纠偏 | 后果 |
|---|---|---|
| 用「画笔画线」当需求 | 拆成「坐标点有序事件流」 | 协议设计失去锚点 |
| 把多人当「能同时在线」 | 拆成「扇出 + 合并」两笔账 | 热房间必卡死 |
| 把回放当「录个屏」 | 拆成「只增日志可重放」 | 撤销与分享都做不了 |
这三处误区有个共同点:都把「结果」(能画、能多人、能看回放)当成了「需求」,而没有拆到「要实现结果必须满足的模块级性质」。需求拆解的价值,正在于把这种结果语言翻译成可验收、可设计的性质语言——收敛速度、扇出倍数、日志覆盖率。翻译不出来,后面每一章都会反复返工,因为没人知道「做到什么程度算做到」。把这三处误区记下来,每开一个迭代前对照一遍,能省掉大量「以为做完了其实没做」的返工。
一张需求矩阵交给协作方
拆完之后,建议把三条需求整理成一张需求矩阵,贴在协作 wiki 上,让产品、前端、后端、QA 看的都是同一张表。矩阵里每个单元格写「谁负责、验收指标是什么、第几版交付」,这样需求不再是口口相传的传说,而是可追踪的契约。经验上,协作白板项目扯皮最多的就是「实时到底多快才算快」,有了矩阵,这句话变成「P95 收敛小于 100ms」,争论当场结束一半。
| 需求 | 负责方 | 验收指标 | 交付版本 |
|---|---|---|---|
| 实时收敛 | 后端 + 前端 | P95 < 100ms | V1 |
| 多人扇出 | 后端 | 千人房间线性可扩 | V1 |
| 冲突合并 | 后端 | 冲突窗口 50ms 内无分叉 | V1 |
| 回放覆盖 | 后端 | 每笔入流,可重放 | V2 |
需求矩阵还有一个隐性好处:QA 拿到它就知道该压测什么、该回归什么,省下「测试不知道测到什么程度」的沟通成本。
踩坑提示
最典型的是「把三条需求当一条做」:团队以为「做实时协作白板」是一件事,结果实时做完了、多人和可回放还没动,上线发现「能画但没人在看」,白白浪费一轮。三条要分别定验收指标:实时看收敛速度(P95 < 100ms)、多人看扇出倍数(千人房间可压到线性)、可回放看日志覆盖率(每笔入流、可重放)。
小结
实时要状态收敛速度,多人要扇出加合并,可回放要事件流。三条一起定:白板后端的核心不是「画」,而是把一笔笔触变成一条有序、可重放、可合并的事件流。把这三条拆到模块级,后面选型和协议才有靶子。
延伸阅读
- 下一节 1.2 选型对比,见 Node.js 云开发实战 的长连接迁移对照。
- 共享态与事件顺序,见 Go 微服务实战。
- 只增日志流与削峰,见 Rust 实战。
- 协作小站的实时同步,见 K8s 实战 的多环境交付视角。
