CRDT 协同编辑:如何确定操作时序?

大家好,我是前端西瓜哥。

CRDT 协同编辑中,我们经常会使用 Last-Writer-Win 的策略解决冲突。即对于多个冲突的操作,哪个操作是最后修改的,就应用哪个操作。

于是这里就有一个问题,我们应该基于什么去做判断先后顺序?

服务端时间

开发普通 Web 应用的时候,我们不用考虑客户端状态是否同步,只需维护好服务端的一份数据,晚到达的写操作会覆盖掉旧数据,可以视为基于 请求到达服务器时间戳 的比较。

这在协同编辑的场景中能用吗?

不太行。

这种方案需要一个服务器,所以就无法支持去中心化的 P2P 协同编辑(不经过服务器,用户直接向用户发送数据)。

假设可以提供中心服务,如果有客户端 A 在离线的状态下进行编辑,在很长一段时间后才重连同步。到他再次重连的这段期间,其他客户端可能进行了操作。

最后客户端 A 重连服务器并进行同步,客户端 A 的操作最早,但优先级却最高,覆盖掉了其他客户端的编辑,这并不合理。

客户端时间

既然要考虑离线的情况,那我们转换一下思路,使用客户端时间戳如何?

我们在操作生成的那一刻,把当前客户端时间也加上,这样同步后就能知道操作的先后顺序了。

理论上是可以的,但有 准确性问题:客户端的物理时钟无法同步一致。

尤其是有些客户端的系统时间错得离谱,比如比真实时间晚一分钟或晚好几天。

如果客户端是可管理,比如分布式系统的节点,我们可以通过 NTP 时间同步服务进行时间校准,误差在 100 ms 以内,如果是局域网会小很多。此外还可以使用更高精度的时钟硬件。

当然我们这里讨论的是协同编辑,客户端是不可控的,就不发散思考了。

不同步的客户端时间戳会导致因果错乱的问题。

比如客户端 A 创建一个对象,同步给客户端 B,然后 B 将其删除。这两个操作是不能颠倒过来的。

但在使用客户端时间戳的场景下是可能发生的:在客户端 A 的系统时间比客户端 B 的系统时间晚一些,那创建操作的时间戳就可能会比删除操作晚一点。

客户端的物理时钟不可靠,我们需要另一种时钟:逻辑时钟。

逻辑时钟

逻辑时钟是人为定义的递增序列,只要能表示两个操作的先后顺序即可。

对于协同编辑,我们通常会选择 Lamport 逻辑时钟算法。

对客户端 A,初始化时会有一个全局的逻辑时钟 clockID,通常我们选择从某个数字开始。

客户端 A 的每进行一个操作 operation,clockID 加一,然后保存到对应的 operation 操作对象上:

ini 复制代码
let clockID = 0;
let sessionID = 3;

const op = {
  id: {
    sessionID: sessionID, 
    clockID: ++clockID,
  }
  // ...
}

这里还要保存当前客户端的唯一 ID,后面再说有什么用。

当要将操作同步给其他客户端 B 时,生成一个专门的 "发送消息操作对象",带上本地的 clockID。

操作到达客户端 B 后,此时将本地逻辑时钟更新为 clockID(A) 和 clockID(B) 的最大值,然后加一,这里的目的是对齐,确保之后操作的发生时间都大于 clockID(A) 以及本地的 clockID(B)。

接着就是生成专门的 "接收消息操作对象"。

大概如上所示。

上面这些节点的存在偏序关系,即部分有序。

比如客户端 A 创建的递增节点,

rust 复制代码
A:0 -> A:1 -> A:2 -> A:3

以及客户端 B 创建的递增节点:

rust 复制代码
B:0 -> B:1 -> B:2 -> B:3 -> B:4

还有同步前节点到同步节点。

rust 复制代码
A:0 -> A:1 -> A:2 -> B:3 -> B:4

这些节点之间的因果关系是明确的,但对于 A0 和 B0,A1 和 B0 这些无法确认因果关系的节点,我们认为它们是 并发关系。

并发关系的话,可以理解两个平行时空发生的两件事,谁先发生谁后发生并不重要,但我们要 找到一个方式给它们排个序,让偏序变成全序(完全有序),这样才能让所有客户端的最终结果是一致的。

所以对于所有节点,我们这样排序:

clockID 小的先发生,如果 clockID 相同,sessionID 小的先发生。

这里不一定要 sessionID 小的先发生,大的也可以,只要确保比较结果一致且满足传递性。

于是同步后节点的操作顺序是:

至此,操作顺序就确定好了,且符合最终一致性原则。

结尾

我是前端西瓜哥,关注我,学习更多协同编辑知识。


相关阅读,

CRDT 协同编辑:修改树的节点层级 Mutable Tree Hierarchy

CRDT 协同编辑:另一种顺序一致性算法 Tree-Based Indexing

Figma 在协同编辑中使用的顺序一致性算法:Fractional indexing

Figma 是如何做协同编辑的?

这一次,彻底搞懵 CRDT

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万4 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋4 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学4 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端