可視化 - 多人協同技術原理和案例分享

前言

hi,大家好,我是徐小夕,之前和大家分享了很多可視化低代碼的技術實踐,最近也做了一款非常有意思的文檔搭建引擎——Nocode/Doc

也做了一些分享:

接下來和大家分享另一個比較有意思的話題——多人協同技術

文章大綱

多人協同技術方案探討

多人協同技術方案常見的應用場景主要有:

主要目的是實現多個人同時編輯一份共享資源, 來提高工作效率。

拋開已有技術本身,我們拿最簡單的富文本編輯器爲例子, 如果我們想讓它實現多人同時編輯,有哪些可以想到的方案呢?

即每個人保存時都強制以自己的版本爲主,即保存最後一次修改,這樣會導致的問題是無法實現真正意義上的共享協作。

也就是對文件” 上鎖 “。當某個用戶正在編輯文檔時,對此文檔進行加鎖處理,避免多人同時編輯,從而避免文檔的內容衝突。 缺點就是用戶體驗不友好,並且需要等待時間。

我們可以採用類似 git 的版本管理模式,多人編輯時利用 webrtc / socket 與服務端通信,保存時通過服務端進行差異對比、合併,自動進行衝突處理,再通過服務推送如SSE(服務端實時主動向瀏覽器推送消息的技術) 的方式推送給其他人。

弊端是會出現類似 git 修改同一行,純靠服務端無法處理,需要手動處理衝突。

這裏給大家推薦一個有意思的庫 NodeGit

github 地址: https://github.com/nodegit/nodegit

以下是 NodeGit 的一些主要特點:

NodeGit 可以用於多個領域,例如自動化部署、協作工具、代碼分析、教育工具和 CI/CD 系統等。通過使用 NodeGit,我們能以編程方式訪問和操作 Git 存儲庫,實現更靈活和自動化的版本控制流程。

當然以上這幾種方式很難應對複雜場景的多人協作。

OT 和 CRDT 算法

OT 算法是一種用於實時協同編輯的算法,它通過操作 & 轉換來實現數據的一致性。在 OT 算法中,每個用戶對數據的操作(如修改、刪除等)都被記錄下來,並在其他用戶的客戶端進行相應的轉換,從而實現多個用戶對同一份數據的協同編輯。

OT 算法的優點在於它可以實時地反映用戶的操作,並且可以很好地處理併發衝突。但是 OT 算法需要在中心化的服務器上進行協同調度,因此對於大規模的分佈式系統來說不太適用。

操作 Operational

基於 OT 的協同編輯核心是:將文檔的每一次修改看作是一個操作,即操作原子化處理,如在第 N 個位置插入一個字符時,客戶端會將操作發送到服務端去處理。

quill富文本編輯器舉例, 它通過 retaininsertdelete 三個操作完成整篇文檔的描述與操作,如下:

 [
    // Unbold and italicize "Gandalf"
    { retain: 7, attributes: { bold: null, italic: true } },

    // Keep " the " as is
    { retain: 5 },

    // Insert "White" formatted with color #fff
    { insert: 'White', attributes: { color: '#fff' } },

    // Delete "Grey"
    { delete: 4 }
  ]

相關地址:https://quilljs.com/docs/delta

Transformation 轉換

用戶將原子化的操作發送到服務端時(必須有中央服務器進行調度), 服務端對多個客戶端的操作進行轉換,對客戶端操作中的併發衝突進行修正,確保當前操作同步到其他設備時得到一致的結果,因爲對沖突的處理都是在服務端完成,所以客戶端得到的結果一定是一致的,也就是說 OT 算法的結果保證強一致性。

轉換完成後,通過網絡發送到對應用戶,用戶合併操作,從而得到一致結果。

這意味着 OT 算法對網絡要求更高,如果某個用戶出現網絡異常,導致一些操作缺失或延遲,那麼服務端的轉換就會出現問題。

OT 算法可視化模型:https://operational-transformation.github.io/index.html

CRDT

CRDT 算法全稱 Conflict-free Replicated Data Type,即無衝突複製數據類型,是一種基於數據結構的無衝突複製數據類型算法,它通過數據結構的合併來實現數據的一致性

CRDT 算法中,每個用戶對數據的修改都會被記錄下來,並在其他用戶的客戶端進行合併,以實現數據的一致性。CRDT 算法的優點在於它可以適用於大規模的分佈式系統,並且不需要中心化的服務器進行協同調度。

但是,CRDT 算法在處理複雜操作時可能會存在合併衝突的問題,需要設計複雜的合併函數來解決。

Yjs 是專門爲在 web 上構建協同應用程序而設計的 CRDT.

CRDT 包含以下兩種:

基於狀態的 CRDT 更容易設計和實現,每個 CRDT 的整個狀態最終都必須傳輸給其他每個副本,每個副本之間通過同步全量狀態達到最終一致狀態,這可能開銷很大;

而基於操作的 CRDT 只傳輸更新操作,各副本之間通過同步操作來達到最終一致狀態,通常很小。

穿插一個小概念:

向量時鐘(Vector Clock),它是一種在分佈式系統中用於記錄事件順序的時間戳機制。它的主要目的是在分佈式環境中實現事件的併發控制和一致性。

向量時鐘的基本思想是爲系統中的每個節點維護一個向量,其中每個分量對應一個節點,用於記錄該節點的事件發生次數。當一個節點發生事件時,它會增加自己分量的值。向量時鐘的關鍵是在不同節點之間傳遞這些向量,並在合併時確保一致性。

目前協同算法底層都會採用向量時鐘的模式來設計操作算法。

插曲(互斥鎖(Mutex)原理和代碼實現)

先上代碼:

const createMutex = () ={
    let token = true
    return (f, g) ={
        if (token) {
            token = false
            try {
                f()
            } finally {
                token = true
            }
        } else if (g !== undefined) {
            g()
        }
    }
}

它用於創建一個互斥鎖(Mutex)。互斥鎖是一種用於控制資源訪問的機制,確保在任何給定的時間只有一個線程(在這裏可以理解爲一個函數調用)可以訪問被保護的資源或代碼塊

下面是對代碼中每個部分的解釋:

通過這種方式,createMutex 函數創建了一個簡單的互斥鎖機制。只有在互斥鎖可用時,才能執行 f 函數。如果互斥鎖已被其他函數獲取,將跳過 f 函數的執行,並在可能的情況下執行 g 函數。

這種互斥鎖的實現通常用於在多線程或異步環境中確保對共享資源的安全訪問

yjs 協同框架使用

Yjs 本身是一個數據結構,原理是:當多人協作時,對於文檔內容修改,通過中間層將文檔數據轉換成 CRDT 數據;通過 CRDT 進行數據數據更新這種增量的同步,通過中間層將 CRDT 的數據轉換成文檔數據,另一個協作方就能看到對方內容的更新。

中間內容的更新是基於 Yjs 數據結構進行的,衝突處理等核心都是 Yjs 承擔的,通信基於 websocketwebrtc,所以我們只需要簡單的使用就可以實現多人協同的應用。

下面是我總結的一個結構:

Yjs 基於數據結構層面處理衝突,比 OT 更加穩健,對複雜網絡的適應性更強。網絡延時或離線編輯對數據結構來說,處理沒有任何差異。

我總結了一下它的幾個核心特點:

Yjs 提供的 Awareness(意識)模塊,名如其意,讓協作者能夠意識到其他人的位置在哪,有效避免衝突可能性。

基於 CRDT 的內容合併,天然支持離線編輯,瀏覽器端做本地化存儲。

Yjs 自身提供了快照機制,保存歷史版本不用保存全量數據,只是基於 Yjs 打一個快照,後續基於快照恢復歷史版本。

上限人數很高,可支持很多人同時編輯。

目前主流的 figma 也是採用的 CRDT 開發協同編輯功能。

yjs 使用

以上我根據自己的理解整理了一下 yjs 的核心模塊。接下來我以數組結構爲例子給大家介紹一下它的用法:

import * as Y from 'yjs'

const ydoc = new Y.Doc()

// 1: 定義一個類型爲數組的共享數據結構
const yarray = ydoc.getArray('my doc') 


// 2. 向數組中插入數據,在第一個位置插入3條數據
yarray.insert(0, [1, 2, 3]) 
// 3. 在第二個位置刪除一條數據
yarray.delete(1, 1)
// 4. 獲取可用的結果
yarray.toArray() // =[1, 3]

// 5. 監聽數據變化,執行操作
yarray.observeDeep((event) ={
  console.log(event)
})

// 將連續的操作合併到transact 中
ydoc.transact(() ={
  yarray.insert(1, ['a''b'])
  yarray.delete(2, 2) // deletes 'b' and 2
}) // =[{ retain: 1 }{ insert: ['a'] }{ delete: 1 }]

transact方法用於執行事務操作。事務是共享文檔上的一系列更改,這些更改會在一個事務中進行處理,以保證數據的一致性和正確性。每個事務都會觸發Observer調用和update事件,我們可以在這些事件中進行相應的處理。

通過將更改捆綁到單個事務中,可以減少事件調用的次數,並確保數據的一致性。在事務中,我們可以進行多種操作,如插入、刪除、修改等。

yjs 多人協同案例

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/tKvI5DgRYYxTcz__zA_Z0A