一、 实时协作的挑战:幻觉与真相
在 Web Excel 这种高频交互的应用中,用户感受到的“实时”其实是一种技术构建出来的“幻觉”。
由于网络延迟的物理存在,用户 A 的修改传到服务器需要时间,服务器再广播给用户 B 也需要时间。在这个“时间差”里,用户 B 可能也进行了一次修改。如果系统只是简单地按照“谁最后到达服务器谁就生效”的规则(Last Write Wins),那么先到达的数据就会被覆盖,用户的工作成果会无故丢失。
更糟糕的是,如果两人操作的是位置相关的逻辑(例如: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): 该操作对应的文档版本号。
通过
src和seq的组合,服务器可以检测网络不稳定导致的重复提交;通过v版本号,服务器可以判断该操作是否已经过时,是否需要进行转换。
三、 OT 算法的实战:从“xyz”到“xaybz”的冲突之旅
为了通俗易懂地解释 OT,我们参考 SpreadJS 产品文档中的经典案例:
初始状态: 共享文档中只有三个字符 "xyz"。 并发场景:
用户 A: 意图在位置 1(x 和 y 之间)插入字符
"a",期望结果:"xayz"。用户 B: 意图在位置 2(y 和 z 之间)插入字符
"b",期望结果:"xybz"。
场景 1:如果没有 OT(导致数据不一致)
服务器先收到 A 的操作,应用后文档变为
"xayz",并广播给 B。服务器接着收到 B 的操作(位置 2,插入
"b")。服务器在"xayz"的位置 2 插入"b",结果变为"xabyz"。冲突发生: 当 B 的原始操作(位置 2,插入
"b")传给 A 时,A 当前的文档是"xayz"。如果在位置 2 插入"b",A 的屏幕上会显示"xabyz"。而当 A 的操作传给 B 时,B 当前的文档是其本地修改后的
"xybz"。如果在位置 1 插入"a",B 的结果变成了"xaybz"。
结果: A 看到的是 "xabyz",B 看到的是 "xaybz"。两边数据不一致,系统崩溃。
场景 2:有了 OT 的优雅转换
OT 的精髓在于 transform 函数。当 B 的操作到达 A 时,A 并不直接执行它,而是先问一下系统:“在我已经执行了 A 操作的前提下,B 操作应该怎么变?”
转换逻辑: 既然 A 已经在位置 1 插入了一个长度为 1 的字符
"a",那么原来在位置 2 的操作意图,现在应该“自动往后挪一位”,变成在位置 3 插入。执行结果: A 执行转换后的 B 操作(位置 3 插入
"b"),得到"xaybz"。对称同步: 同理,B 在接收 A 的操作时,判定 A 的插入位置(pos 1)在自己操作位置(pos 2)之前,因此 A 的操作位置不需要变。B 执行 A 操作后,结果也是
"xaybz"。最终: 全局一致,冲突消失。
四、 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?
有些简单的同步方案采用的是“全量结果同步”,即每次修改都把整个单元格甚至整个表格发给后端。这在企业场景下是不可接受的:
性能开销: 巨型 Excel 文件动辄几十 MB,全量传输会瞬间拖垮网络。
语义丢失: 结果同步无法表达“插入一行”这种语义。如果两个人同时插入行,结果同步会导致其中一个人的插入被完全抹除;而 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 报表设计等业务场景需求,极大的降低企业研发成本和项目交付风险。
如下资源列表,可以为您评估产品提供帮助:
葡萄城热门产品