揭開在線協做的神祕面紗 - 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前端工程師(社招)

相關文章
相關標籤/搜索