手机上和 Agent 聊到一半,切到电脑端上下文没对齐;两个人同时改同一份提示词,后写入的把先写入的覆盖掉——多端 AI 会话的两个经典故障,根源都是拿中心化锁机制去解决本该离线优先的问题。无冲突复制数据类型(CRDT)给出另一条路:不加锁、支持离线编辑、网络恢复时数学上保证收敛。本文拆 Yjs 的落地实现和收敛性的验证方法。
为什么锁机制在此行不通
传统方案靠服务端串行化写入:WebSocket 断连时写入丢失,弱网下体验崩塌,边缘 Serverless 环境更是没有常驻锁可用。CRDT 的思路相反——每个端先在本地自由写入,数据结构本身保证任意两个副本按任意顺序合并都得到同一结果。这叫强最终一致性:不依赖合并时机,只依赖数据结构性质。
Yjs 实现骨架
核心类持有三样东西:Y.Doc 文档实例、共享数组(消息列表挂在上面)、两个 Provider——y-indexeddb 做本地离线持久化,y-webrtc 做 P2P 组网同步。
this.doc = new Y.Doc();
this.messagesArray = this.doc.getArray<ChatMessageItem>(`session-${sessionId}`);
this.persistence = new IndexeddbPersistence(`cyber-chat-${sessionId}`, this.doc);
this.provider = new WebrtcProvider(`cyber-mesh-${sessionId}`, this.doc, {
signaling: ['wss://signaling-a', 'wss://backup-b'],
});
追加消息用 doc.transact 包住 push——事务保证原子广播,在线端即时收到,离线端落在 IndexedDB 里排队,联网后自动同步。变更监听用 observe 驱动 React 状态更新,销毁时三个资源按反序释放。
接入层要点:会话 ID 既做数组键名也做同步房间名,一 session 一频道,互不串线;信令服务配主备两个,信令挂了 P2P 通道建不起来,这是可用性的第一瓶颈。
收敛性怎么验证
"数学上保证收敛"不能只当口号,要测三组用例:
- 并发追加:两端离线状态下各追加 N 条消息,恢复连接后比对两端消息序列——长度、顺序、内容逐项一致才算过。
- 乱序到达:人为延迟一端的消息包,让同步乱序送达,最终视图仍须一致。CRDT 的收敛不依赖到达顺序,测试必须覆盖这一点。
- 断连重启:写入后杀进程,重开页面从 IndexedDB 恢复,再同步远端增量——本地持久化和远端合并不能互相覆盖。
验证依据是 Lamport 时间戳与向量时钟:每个操作带因果序元数据,合并时按确定规则排位。理解这一点就能解释为什么并发消息的最终顺序可能和各端的"直觉顺序"不同——排序由数据结构决定,不由到达时间决定,产品层要接受这个语义。
选型的三条边界
适用场景明确:高频小消息、需要离线能力、多端或多用户并发的会话类应用。不适用也要明确:大文件同步(CRDT 元数据开销随内容膨胀)、强一致性要求高的金融写入(最终一致不等于实时一致)、纯单端应用(引入 CRDT 是纯开销)。
成本上还有一笔账:WebRTC P2P 省掉了中心数据库的存储与吞吐,但信令服务器仍需自建或托管;IPFS 快照做阶段性归档时,快照触发的时机要按消息量而非时间定,否则恢复窗口不可控。用数据结构替代互斥锁,省的是运维复杂度,付的是理解成本——这个交换对多端协同产品通常是划算的。

评论0