【技术本质篇】深度解析 OT (操作转换) 算法:如何优雅地解决多人编辑冲突?

在 Web Excel 这种高频交互的应用中,用户感受到的“实时”其实是一种技术构建出来的“幻觉”。 由于网络延迟的物理存在,用户 A 的修改传到服务器需要时间,服务器再广播给用户 B 也需要时间。在这个“时间差”里,用户 B 可能也进行了一次修改。如果系统只是简单地按照“谁最后到达服务器谁就生效”的规则(Last Write Wins),那么先到达的数据就会被覆盖,用户的工作成果会无故丢失

发布于 2026/03/04 14:47

SpreadJS

一、 实时协作的挑战:幻觉与真相

在 Web Excel 这种高频交互的应用中,用户感受到的“实时”其实是一种技术构建出来的“幻觉”。

由于网络延迟的物理存在,用户 A 的修改传到服务器需要时间,服务器再广播给用户 B 也需要时间。在这个“时间差”里,用户 B 可能也进行了一次修改。如果系统只是简单地按照“谁最后到达服务器谁就生效”的规则(Last Write Wins),那么先到达的数据就会被覆盖,用户的工作成果会无故丢失。

img

更糟糕的是,如果两人操作的是位置相关的逻辑(例如:Alice 在第 5 行插入数据,而 Bob 删除了第 2 行),如果不进行逻辑转换,Alice 的数据最终可能会出现在错误的位置。

OT 算法的核心使命,就是让每个客户端在接收到远程指令时,能够根据自己当前的上下文环境,对该指令进行“二次加工(转换)”,从而达成全局一致。

二、 解剖操作意图:什么是 Op (Operation)?

在 SpreadJS 的协同世界里,所有的变更都被抽象为 Op(操作)。Op 是对文档状态单次修改的严谨描述。

1.客户端 Op:纯粹的操作描述

当你在 SpreadJS 单元格输入“Hello”时,客户端会产生一个简单的 Op:

// 示例:在文档位置 1 插入文本 'hello'
sharedDoc.submitOp({ pos: 1, text: 'hello' })

2.服务端 Op:带有元数据的“身份牌”

当这个 Op 到达协同服务器后,js-collaboration-ot 模块会为其添加元数据,使其具备可追溯性和唯一性:

  • src (Source Identifier): 标识是哪个用户提交的。

  • seq (Sequence Number): 该用户提交操作的序号。

  • v (Version): 该操作对应的文档版本号。

    通过 srcseq 的组合,服务器可以检测网络不稳定导致的重复提交;通过 v 版本号,服务器可以判断该操作是否已经过时,是否需要进行转换。

    img

三、 OT 算法的实战:从“xyz”到“xaybz”的冲突之旅

为了通俗易懂地解释 OT,我们参考 SpreadJS 产品文档中的经典案例:

初始状态: 共享文档中只有三个字符 "xyz"并发场景:

  • 用户 A: 意图在位置 1(x 和 y 之间)插入字符 "a",期望结果:"xayz"

  • 用户 B: 意图在位置 2(y 和 z 之间)插入字符 "b",期望结果:"xybz"

场景 1:如果没有 OT(导致数据不一致)

  1. 服务器先收到 A 的操作,应用后文档变为 "xayz",并广播给 B。

  2. 服务器接着收到 B 的操作(位置 2,插入 "b")。服务器在 "xayz" 的位置 2 插入 "b",结果变为 "xabyz"

  3. 冲突发生: 当 B 的原始操作(位置 2,插入 "b")传给 A 时,A 当前的文档是 "xayz"。如果在位置 2 插入 "b",A 的屏幕上会显示 "xabyz"

  4. 而当 A 的操作传给 B 时,B 当前的文档是其本地修改后的 "xybz"。如果在位置 1 插入 "a",B 的结果变成了 "xaybz"

结果: A 看到的是 "xabyz",B 看到的是 "xaybz"。两边数据不一致,系统崩溃。

场景 2:有了 OT 的优雅转换

OT 的精髓在于 transform 函数。当 B 的操作到达 A 时,A 并不直接执行它,而是先问一下系统:“在我已经执行了 A 操作的前提下,B 操作应该怎么变?”

  1. 转换逻辑: 既然 A 已经在位置 1 插入了一个长度为 1 的字符 "a",那么原来在位置 2 的操作意图,现在应该“自动往后挪一位”,变成在位置 3 插入。

  2. 执行结果: A 执行转换后的 B 操作(位置 3 插入 "b"),得到 "xaybz"

  3. 对称同步: 同理,B 在接收 A 的操作时,判定 A 的插入位置(pos 1)在自己操作位置(pos 2)之前,因此 A 的操作位置不需要变。B 执行 A 操作后,结果也是 "xaybz"

    最终: 全局一致,冲突消失。

    img

四、 SpreadJS 协同框架中的 OT 实现

在 SpreadJS 协同插件中,开发者无需手动编写复杂的转换逻辑,js-collaboration-ot 已经封装好了核心底座。

1.OT 类型的定义

通过 OT_Type 接口,系统定义了如何创建快照、如何转换操作以及如何应用操作。

codeTypeScript

export interface OT_Type<S = unknown, T = unknown> {
  uri: string; // 类型唯一标识
  transform(op1: T, op2: T, side: 'left' | 'right'): T; // 核心转换函数
  apply(snapshot: S, op: T): S; // 应用操作
}

其中的 side 参数('left' 或 'right')非常巧妙,它用于处理“平局”情况:如果两个用户同时在同一个位置插入字符,系统通过 side 约定谁的字符排在前面,确保无论在哪个客户端,转换结果都绝对统一。

2.注册与绑定

开发者只需要在客户端和服务端通过 TypesManager.register(type) 注册相同的 OT 类型,SpreadJS 就会自动接管后续的冲突处理。

对于 SpreadJS 的专用表格协同,spread-sheets-collaboration-addon 已经内置了符合 Excel 逻辑的复杂 OT 类型。它不仅能处理简单的字符串,还能处理单元格属性、行列增减、公式重新计算等复杂的表格逻辑。

五、 意图同步 vs 结果同步:为什么 OT 更适合企业级 Excel?

有些简单的同步方案采用的是“全量结果同步”,即每次修改都把整个单元格甚至整个表格发给后端。这在企业场景下是不可接受的:

  1. 性能开销: 巨型 Excel 文件动辄几十 MB,全量传输会瞬间拖垮网络。

  2. 语义丢失: 结果同步无法表达“插入一行”这种语义。如果两个人同时插入行,结果同步会导致其中一个人的插入被完全抹除;而 OT 能够理解“插入”的意图,最终结果是增加了两行。

    SpreadJS 采用 OT 算法,实现了真正的“意图同步”。 它传输的是轻量级的指令流,这使得它在处理超大型文档和高并发协作时,依然能保持极低的延迟和极高的准确度。

六、 总结:严谨,是协同的第一要素

对于金融、财务、生产制造等企业级应用来说,数据的准确性高于一切。SpreadJS 协同插件通过底层的 js-collaboration-ot 模块,将深奥的操作转换理论转化为开箱即用的能力。

OT 算法不仅仅是解决冲突,更是对用户劳动成果的尊重。 它确保了每一处修改都能被正确地理解、转换并应用,让多人在线协作从“敢看不敢改”变成了真正的“放心协作”。

在了解了冲突解决的底层逻辑后,你可能会好奇:在视觉上,我们如何知道其他用户正在做什么?如何避免两个人在完全不知道对方存在的情况下修改同一个格子?

下一篇文章,我们将聊聊协同中的“上帝视角”——Presence 插件与实时状态共享。敬请期待。

技术要点回顾:

  • Op 是协作的最小单元,包含操作位置、内容及版本元数据。

  • Transform (转换) 是 OT 的核心,通过调整 Op 的位置偏移处理并发冲突。

  • 一致性:OT 确保了所有客户端在处理完相同序列的 Op 后,文档状态绝对相等。

  • SpreadJS 优势:内置针对 Excel 逻辑优化的 OT 类型,支持复杂表格操作。

SpreadJS | 下载试用

纯前端表格控件SpreadJS,兼容 450 种以上的 Excel 公式,具备“高性能、跨平台、与 Excel 高度兼容”的产品特性,备受华为、苏宁易购、天弘基金等行业龙头企业的青睐,并被中国软件行业协会认定为“中国优秀软件产品”。SpreadJS 可为用户提供类 Excel 的功能,满足表格文档协同编辑、 数据填报、 类 Excel 报表设计等业务场景需求,极大的降低企业研发成本和项目交付风险。

如下资源列表,可以为您评估产品提供帮助:

相关产品
推荐相关案例
推荐相关资源
关注微信
葡萄城社区二维码

关注“葡萄城社区”

加微信获取技术资讯

加微信获取技术资讯

想了解更多信息,请联系我们, 随时掌握技术资源和产品动态