揭开在线协做的神秘面纱 - OT算法

原文:揭开在线协做的神秘面纱 - OT算法 | AlloyTeam
做者:TAT.jayjavascript

相信你们或多或少都有使用过在线文档,国内的像咱们在作的腾讯文档还有其余家的不少相似产品。今天主要为你们揭开在线协做的神秘面纱,那就是OT算法。前端

0x01 背景

在线文档,抽象一下,这些产品的模式都是富文本编辑器+后台,富文本编辑器产生内容,展现内容,而后后台负责保存。 富文本编辑器如今业界已经有不少成熟的产品,像codeMirror,这一块自己也是很复杂的一块,也不是我们此次关注的重点方向。 不知道你们日常在用这些产品的时候有没有思考过一个问题,在线文档编辑的时候产生冲突怎么办?java

0x02 举个栗子

举个很简单的例子,如今你们的文本都是‘aaab’,A用户在第3个字符行后面插入了一个‘c’,B用户在第3个字符行后面插入了一个‘d’,这个时候A这边看到的是‘aaacb’,B这边看到的是‘aaadb’,咱们假设A用户先提交了数据,那其实最后预期的数据其实应该是‘aaacdb’,这样就最大的保存了每一个人的输入。 那咱们如今来看看正常状况下这里会发生什么: A用户:git

A本地已是‘aaacb’了,过一下子,后台告诉它B也编辑了,编辑的行为就是第3个字符行后面插入了一个‘d’,那A这边执行了这个行为,最终变成了‘aaadcb’github

B用户:算法

B本地已是‘aaadb’了,过一下子,后台告诉它A也编辑了,编辑的行为就是第3个字符行后面插入了一个‘c’,那B这边执行了这个行为,最终变成了‘aaacdb’后端

从上面的模拟过程能够看到,A用户最后的结果实际上是不正确的,可是B是正确的。数组

这里先解释一下你们可能会疑惑的地方:为何B是过一下子后台告诉它A编辑了,不是说A先提交了数据吗? 其实这里针对的是冲突场景,这里若是B在提交以前,已经收到后台告诉它A编辑了,那其实就是顺序编辑了,也就不是冲突了。因此这里指的是A,B几乎同时提交,可是A到达后台仍是快一点的,也就是A,B在编辑的时候是不知道彼此的存在的。服务器

真实的冲突场景其实不是这种简单的时序问题,这里我后面再介绍。前端工程师

0x03 尝试解决

这里可能有一些聪明的小伙伴有了一些想法。

解决方案一:丢了丢了

这多是最简单粗暴的方法了,我发现有冲突,就告诉用户,主子,咱这里有冲突了,臣妾解决不了啊。可是显然这会常常出现,而后主子就把你打入冷宫了。

解决方案二:锁

有些小伙伴想到,上面出现问题,还不是由于你们编辑了都当即应用了,咱们编辑后不当即应用不就行了,并且历史告诉咱们,有冲突加锁应该能够解决。那咱们看看假如不当即应用,咱有没有什么处理办法: A用户:

A本地已是‘aaab’了,A编辑了,可是不该用,先发后台

B用户:

B本地已是‘aaab’了,B编辑了,可是不该用,先发后台

后台:

后台先收到A请求,而后加了一个锁,而后收到了B请求,这时侯应该是加锁的状态,因此接受了A,拒绝了B

A用户:

A用户收到了后台的回复,告诉它你的提交我接收了

B用户:

B用户收到了后台的回复,告诉它你的提交被我拒绝了,由于冲突了

这样虽然能继续下去,可是好像仍是不太行的亚子啊,B的提交仍是丢了,因此好像和第一种简单粗暴的方法没啥区别

0x04 OT算法

顺其天然的,这个时候你会看到OT算法驾着七彩祥云来救你了~ 其实回到上面的例子,本质问题仍是由于后台通知你们的B的编辑行为看起来不太对。如今后台通知你们的是:

B的编辑的行为就是第3个字符行后面插入了一个‘d’

可是在A已经接受的状况下,正确的通知应该是:

B的编辑的行为就是第4个字符行后面插入了一个‘d’

假如咱们把A提交的行为叫作A,B提交的行为叫作B,如今后台就是一个简单的转发功能,告诉A的是B,告诉B的是A,而后就出现问题了。因此后台应该更聪明一点,它应该学会一个招术,那就是把每一个人提交的行为转变一下再告诉别人,其实这个技术就是OT算法。

OT算法全名叫Operation Transformation,你看从名字就对应了上面我说的转变算法。 假设咱们的OT算法的转换功能叫transform,那transform(A,B)= A',B'。 也就是说你输入两个前后执行的行为,它会告诉你两个转换事后的行为,而后把A'行为告诉B,把B'行为告诉A,这样你们再应用就相安无事了。

上面的图是OT的经典菱形图,也就是说A会变成A'在B这边执行,B会变成B'在A这边执行。 这里实际抽象一下,用户永远就只有两我的,一个是本身,一个是服务端,只是服务端的操做可能来自不少人,若是不这样抽象,那一个个进行冲突处理可能会让你以为没法理解。 那咱们如今再来看看后台有了OT这个能力以后会发生什么:

A用户:

A本地已是‘aaacb’了,过一下子,后台告诉它B也编辑了,编辑的行为就是第4个字符行后面插入了一个‘d’,那A这边执行了这个行为,最终变成了‘aaacdb’

B用户:

B本地已是‘aaadb’了,过一下子,后台告诉它A也编辑了,编辑的行为就是第3个字符行后面插入了一个‘c’,那B这边执行了这个行为,最终变成了‘aaacdb’

如今A、B就一致了!

0x05 OT算法的实现

如今OT算法对咱们来讲就是一个黑盒,咱们知道给必定的输入,它会有正确的输出,可是它是如何作到的呢? 在介绍它的实现以前,咱们须要抽象一下咱们的操做行为,在以前咱们的描述都是

第3个字符行后面插入了一个‘d’

这里怎么转换成程序识别或者能用代码表达的呢?其实这也是OT的关键。 这里我直接揭晓答案: 全部对文本的操做均可以抽象成三个原子行为:

R = Retain,保持操做 I = Insert,插入操做 D = Delete,删除操做

那以前的行为

第3个字符行后面插入了一个‘d’

就会变成

R(3), I('d')

也就是保持三个字符后插入1个‘d’,其实应该也很好理解,这里的操做就像操做数组同样,无论干什么,第一步你得先找到操做的下标。 有了这三个原子之后,咱们就能够看到:

A = R(3),I('c') B = R(3), I('d')

一切准备就绪,咱们能够开始看OT了,这里OT算法如今已经很成熟了,这里我以一个github上的repo为例:ot.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)) {
        // R和R处理
      } else if (isDelete(op1) && isDelete(op2)) {
       	//D和D处理
      } else if (isDelete(op1) && isRetain(op2)) {
       // D和R处理
	  } else if (isRetain(op1) && isDelete(op2)) {
       //R和D处理
      }
    }
    return [operation1prime, operation2prime];
  };
复制代码

这里就是OT的transform实现,本质上就是把用户的原子操做数组拿到之后,而后作transform操做,这里我只选了一小段来大概解析下,具体的能够看注释,其实本来的注释已经很全了。 其实上面那段代码,由于咱们的原子操做只有三种,根据排列组合,最多只会有9种状况,只是上面把不少状况合并了,你要是不理解,也能够拆开,帮助理解。 其实上面的文件还有compose,invert等方法,可是其实transform才是咱们理解的核心,其余方法你们感兴趣能够看看注释和下面贴的一些关于OT更详细介绍的文章。

0x06 OT算法的时序

简单的OT你们只要理解了,好像也并非很难,可是其实真实状况下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这个库。

最后若是读完这篇文章你对在线协做有了必定的认知,那这篇文章的使命也就达到了,最后若是有写的不正确的地方,欢迎斧正~

参考资料

understanding-and-applying-operational-transformation
ot.js


AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: alloyteam@qq.com
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)

相关文章
相关标签/搜索