原文:揭開在線協做的神祕面紗 - OT算法 | AlloyTeam
做者:TAT.jayjavascript
相信你們或多或少都有使用過在線文檔,國內的像咱們在作的騰訊文檔還有其餘家的不少相似產品。今天主要爲你們揭開在線協做的神祕面紗,那就是OT算法。前端
在線文檔,抽象一下,這些產品的模式都是富文本編輯器+後臺,富文本編輯器產生內容,展現內容,而後後臺負責保存。 富文本編輯器如今業界已經有不少成熟的產品,像codeMirror,這一塊自己也是很複雜的一塊,也不是我們此次關注的重點方向。 不知道你們日常在用這些產品的時候有沒有思考過一個問題,在線文檔編輯的時候產生衝突怎麼辦?java
舉個很簡單的例子,如今你們的文本都是‘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在編輯的時候是不知道彼此的存在的。服務器
真實的衝突場景其實不是這種簡單的時序問題,這裏我後面再介紹。前端工程師
這裏可能有一些聰明的小夥伴有了一些想法。
這多是最簡單粗暴的方法了,我發現有衝突,就告訴用戶,主子,咱這裏有衝突了,臣妾解決不了啊。可是顯然這會常常出現,而後主子就把你打入冷宮了。
有些小夥伴想到,上面出現問題,還不是由於你們編輯了都當即應用了,咱們編輯後不當即應用不就行了,並且歷史告訴咱們,有衝突加鎖應該能夠解決。那咱們看看假如不當即應用,咱有沒有什麼處理辦法: A用戶:
A本地已是‘aaab’了,A編輯了,可是不該用,先發後臺
B用戶:
B本地已是‘aaab’了,B編輯了,可是不該用,先發後臺
後臺:
後臺先收到A請求,而後加了一個鎖,而後收到了B請求,這時侯應該是加鎖的狀態,因此接受了A,拒絕了B
A用戶:
A用戶收到了後臺的回覆,告訴它你的提交我接收了
B用戶:
B用戶收到了後臺的回覆,告訴它你的提交被我拒絕了,由於衝突了
這樣雖然能繼續下去,可是好像仍是不太行的亞子啊,B的提交仍是丟了,因此好像和第一種簡單粗暴的方法沒啥區別
順其天然的,這個時候你會看到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,這樣你們再應用就相安無事了。
A用戶:
A本地已是‘aaacb’了,過一下子,後臺告訴它B也編輯了,編輯的行爲就是第4個字符行後面插入了一個‘d’,那A這邊執行了這個行爲,最終變成了‘aaacdb’
B用戶:
B本地已是‘aaadb’了,過一下子,後臺告訴它A也編輯了,編輯的行爲就是第3個字符行後面插入了一個‘c’,那B這邊執行了這個行爲,最終變成了‘aaacdb’
如今A、B就一致了!
如今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更詳細介紹的文章。
簡單的OT你們只要理解了,好像也並非很難,可是其實真實狀況下OT會比想象的還要複雜,由於以前說的菱形會無限拓展。
以前說的時序都是指時間前後順序,衝突也是指同時產生編輯。可是其實這裏的同時不是時間上的同時,而是版本上的同時
。 也就是說咱們須要用一個東西表示每個版本,相似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前端工程師(社招)