OT算法探秘

提到在线文档 绕不开OT算法

  • 目前Google文档、腾讯文档、石墨文档等都是采用了OT算法(从最后链接【如何实现在线多人文档】 腾讯文档的人的文章里面看到的这个结论)

在线文档的多人编辑同一个内容的场景

  • 可以抽象为编辑器和后端

多人同时编辑同一个内容会产生什么问题?

从多人(我 假如我叫A 和其他人)的角度来看,会有冲突

冲突怎么解决?

  • 方式1 可以统一采用一个人编辑的内容
  • 方式2 可以加一个锁,但是加了锁后端最终还是会做取舍,被舍弃的那个内容相当于白编辑了, 等价于方式1

有没有更好的解决方式?

上面问题的本质:本质问题还是因为后台通知其他人(排除编辑的自己)的编辑行为看起来不太对。现在后台通知大家的是: 都采用A的编辑,

如果后端能够在返回的时候,告诉每个人,应该显示什么正确的内容就好了。那大家无论怎么同时编辑,最终看到的都是一样的。

有没有办法实现呢?

这就是OT算法。OT 算法全名叫 Operation Transformation 就可以看出就是要做转操作的转换嘛。

做一次抽象 之前多人编辑,是从编辑者的角度来看的, 但如果换个角度,不看其他编辑者, 而是从 前端(编辑器)和后端的角度来看。 那现在只有两个角色: 那就是 前端编辑的我自己 和后端, 前端我只需要编辑, 后端告诉我正确的内容即可, 有人就会问了,那其他在编辑的人怎么办? 其实对于其他人来说,也只有他/她自己和后端。

假设我们的 OT 算法的转换功能叫 transform,那 transform(A,B)= A',B'。

也就是说你输入两个先后执行的行为,它会告诉你两个转换过后的行为,然后把 A'行为告诉 B,把 B'行为告诉 A,这样大家再应用就相安无事了。

这就是OT 的经典菱形图,也就是说 A 会变成 A'在 B 这边执行,B 会变成 B'在 A 这边执行。

怎么实现的?

所有的用户操作可以做一次抽象: 大致可以分为三种原子行为: 什么都没做 : R = Retain,保持操作

增加了内容 : I = Insert,插入操作

删除了内容 : D = Delete,删除操作

举个例子: 开始文档里面有abc 这个字符串, 用户A 在第四个位置,也就是前三个字符的末尾,插入一个字符串d 那就可以描述为: R(3),I('d')

同理对于另一个正在编辑的用户B来说 也插入一个字符串b,对应的表示就是: R(3), I('b')

这时候 就需要OT算法来处理了,核心的实现是transform 方法 github.com/Operational...

js 复制代码
// Transform takes two operations A and B that happened concurrently and

  // produces two operations A' and B' (in an array) such that

  // `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the

  // heart of OT.

  // 上面这个公式就是OT的核心,它产生了A',B',同时保证执行结果一致,S就是我们开始的状态,可以把这个和菱形图对应起来

  // 整体执行流程有点像合并排序的过程。两个下标指针分别往前走

 

  TextOperation.transform = function (operation1, operation2) {

  // operation1, operation2就是我们的A,B

 

    var operation1prime = new TextOperation(); // 就是A'

    var operation2prime = new TextOperation(); // 就是B'

    var ops1 = operation1.ops, ops2 = operation2.ops;

    var i1 = 0, i2 = 0;

    var op1 = ops1[i1++], op2 = ops2[i2++];

    while (true) {

      // At every iteration of the loop, the imaginary cursor that both

      // operation1 and operation2 have that operates on the input string must

      // have the same position in the input string.

      // 其实这里就是说transform的核心是保证两者的下标一致,这样操作的才是同一个位置的数据

      // ...

      // next two cases: one or both ops are insert ops

      // => insert the string in the corresponding prime operation, skip it in

      // the other one. If both op1 and op2 are insert ops, prefer op1.

      // 如果A是插入操作,A'一定也是插入,但是B'就不一样了,因为A是插入,不管你B是啥,你先等等,所以retain一下,保证下标一致

      // 这里实际上有三种情况,A是插入,B可能是R,I,D

      if (isInsert(op1)) {

        operation1prime.insert(op1);

        operation2prime.retain(op1.length);

        op1 = ops1[i1++];

        continue;

      }

      // 如果B也是插入,那B'就是插入,但是你的A'也得retain一下,保证下标一致

      // 这里可能有两者情况,A可能是R,D

      // 实例化思考一下,A [R(3),I('a')],B [I('b')],那对于A'来说就应该是[R(4),I('a')]

      if (isInsert(op2)) {

        operation1prime.retain(op2.length);

        operation2prime.insert(op2);

        op2 = ops2[i2++];

        continue;

      }

      // ...

      var minl;

      if (isRetain(op1) && isRetain(op2)) {

        //都没变的处理

      } else if (isDelete(op1) && isDelete(op2)) {

        //都删除的处理

      } else if (isDelete(op1) && isRetain(op2)) {

       // 一个删除 一个没变

      } else if (isRetain(op1) && isDelete(op2)) {

       //一个没变 一个删除

      }

    }

    return [operation1prime, operation2prime];

  };

下面是构造函数的定义

三个原子操作的判断方法

是否是没变化

是否是删除

是否是新增

还有一些其他方法的实现 感兴趣可以再看

OT算法的时序问题

刚才举例子是两个人,每人编辑一次的情况, 实际可能更复杂,比如A编辑(不变/插入/删除)了三次, B编辑了一次

之前讨论的是时间维度,同一时间有冲突怎么解决,

这里说的是版本上的冲突, 同一个版本,怎么解决冲突

也就是说我们需要用一个东西表示每一个版本,类似 git 的每次提交,每次提交到服务端的时候就要告诉后端,我的修改是基于哪个版本的修改。

最简单的标志位就是递增的数字。那基于版本的冲突,可以简单理解为我们都是基于 100 版本的提交,那就是冲突了,也许我们并不是同时,谁先到后台决定了谁先被接受而已。这里最夸张的就是离线编辑,可能正常版本已经到了 1000 了,某个用户因为离线了,本地的版本一直停留在 100,提交上来的版本是基于 100 的。

那有了时序的概念,我们再看上面这个菱形,它可以理解成 A 和 B 都基于 100 提交了数据,但是在 A 的提交还没被后台确认的时候,A 又编辑了,但是因为上一次提交没被确认,所以这次不会发到后台,这时服务器告诉它 B 基于 100 做了提交。

这种情况下如何处理,就有点类似于 OT 落地到实践当中,你怎么实现了,上面提到的 github 的那个 repo 的实现其实非常巧妙,你看完注释应该就能全部理解,这里给出代码链接

怎么解决?

还是抽象!!!

精华就在于它把本地分成了几个状态:

Synchronized 没有正在提交并且等待回包的 operation

AwaitingConfirm 有一个 operation 提交了但是等后台确认,本地没有编辑数据

AwaitingWithBuffer 有一个 operation 提交了但是等后台确认,本地有编辑数据

剩下的就是在这三种状态下,收到了本地和服务端的数据,分别应该怎么处理

总结

其实 OT 对应的只是一种思想,具体怎么实现是根据具体情况来区分的,比如我们现在讨论的就是文本的 OT,那有可能图的 OT、表格的 OT 又是其他的实现。OT 的核心就是 transform,而 transform 的核心就在于你怎么找到这样的原子操作了,然后原子操作的复杂度决定了 transform 实现的复杂度。

上面这个 repo 只是帮你实现了文本的协同处理,其实对于在线文档来说,还有样式的冲突处理,感兴趣的可以自己搜索相关资料了解一下,建议精读一下 ot.js 这个库。

一些补充

  • 什么是ProseMirror: juejin.cn/post/718833...
  • 什么是tiptap : The headless rich text editor framework for web artisans.
  • 什么事CRDT: juejin.cn/post/704993... CRDT (conflict-free replicated data type) 无冲突复制数据类型,是一种可以在网络中的多台计算机上复制的数据结构,副本可以独立和并行地更新,不需要在副本之间进行协调,并保证不会有冲突发生。

CRDT 常被用在协作软件上,例如多个用户需要共同编辑/读取共享的文档、数据库或状态的场景。在数据库软件,文本编辑软件,聊天软件等都可以用到它。

相关推荐
聪明的水跃鱼2 分钟前
Nextjs15 构建API端点
前端·next.js
小明爱吃瓜18 分钟前
AI IDE(Copilot/Cursor/Trae)图生代码能力测评
前端·ai编程·trae
不爱说话郭德纲23 分钟前
🔥Vue组件的data是一个对象还是函数?为什么?
前端·vue.js·面试
绅士玖26 分钟前
JavaScript 中的 arguments、柯里化和展开运算符详解
前端·javascript·ecmascript 6
GIS之路28 分钟前
OpenLayers 图层控制
前端
断竿散人29 分钟前
专题一、HTML5基础教程-http-equiv属性深度解析:网页的隐形控制中心
前端·html
星河丶29 分钟前
介绍下navigator.sendBeacon方法
前端
curdcv_po29 分钟前
🤸🏼🤸🏼🤸🏼兄弟们开源了,用ThreeJS还原小米SU7超跑!
前端
我是小七呦29 分钟前
😄我再也不用付费使用PDF工具了,我在Web上实现了一个PDF预览/编辑工具
前端·javascript·面试
G等你下课31 分钟前
JavaScript 中的 argument:函数参数的幕后英雄
前端·javascript