軟體筆記軟體原理:為什麼協作同步
1-3 使用者期待已經被馴化
還有一個容易被忽略的因素:使用者期待已經被 Google Docs、Figma、Notion 這些產品「馴化」了。今天如果你分享一份文件給同事,對方打開卻看到「此文件正在被編輯,請稍後再試」,你的第一反應大概是「這什麼年代的東西」。即時共編已經從加分項變成基本盤,這種期待壓力會逼著所有筆記軟體往同一個方向走。
而這種期待是會層層加碼的。一開始只要「能看到對方改的東西」就好,接著要求「看到對方的游標與選取範圍」,再來要求「離線也能編輯」,然後是「歷史版本可以逐字回溯」、「可以針對段落留言」、「可以多人同時對同一張表格排序」。每一項新功能,都會回頭對同步層提出更高的要求。這就是為什麼協作同步不是一個做完就結束的功能,而是一條需要持續投資的技術長線。
二、協作同步的核心原理:從資料模型開始
要理解同步,必須先理解「被同步的東西是什麼」。筆記軟體的資料模型,決定了同步演算法的選擇空間。同樣是「兩個人同時打字」,如果文件是純文字,處理起來相對單純;如果文件是有階層的區塊結構、帶有巢狀清單、表格、嵌入資料庫、甚至是雙向連結的圖結構,難度就會指數上升。
2-1 三種同步粒度:字元級、區塊級、檔案級
第一種是字元級同步。系統把文件視為一串字元,每一次編輯都被描述成「在第 N 個位置插入字串 S」或「刪除第 N 到 M 個字元」。這種粒度的優點是精細,兩個人同時在同一段落打字時,合併結果可以非常自然,甚至能保留雙方各自的輸入順序。缺點是操作數量龐大,且對結構化內容(例如表格、程式碼區塊)的語意掌握較弱。
第二種是區塊級同步。以 Notion 為代表的區塊編輯器,把文件拆成一連串帶有唯一 ID 的區塊(block),每個區塊內部才是文字。同步的單位因此變成「區塊屬性」與「區塊順序」。這種做法的好處是語意清楚、合併邏輯容易設計,且非常適合結構化內容;缺點是區塊內部的文字仍然需要一套機制來處理同時輸入,而且區塊順序的並發調整(例如兩人同時把同一個區塊拖到不同位置)需要額外的排序演算法。
第三種是檔案級同步。系統只同步整個檔案的內容或差異,例如 Git 式的版本控制,或是以檔案為單位的雲端硬碟同步。這種粒度最粗,衝突處理通常退化為「保留兩份、人工合併」。它的優點是實作簡單、與既有檔案系統相容;缺點是體驗最差,因為使用者必須自己當裁判。
實務上,多數成熟的筆記軟體會採用混合策略。例如底層用區塊模型組織文件結構,區塊內部的文字則用字元級 CRDT 處理;或者是對一般文字用 CRDT,對嵌入的資料庫檢視(例如表格排序、篩選條件)則採用「最後寫入者勝出」的簡單策略,因為這類屬性的衝突影響較小。選擇哪種粒度,本質上是在「合併精細度」與「實作複雜度」之間取捨。
2-2 操作日誌與狀態複製:兩種根本不同的同步哲學
不管粒度為何,同步機制在原理上可以分成兩大類:以操作為中心(operation-based)與以狀態為中心(state-based)。
以操作為中心的思路是:不要傳整份文件,只傳「發生了什麼事」。例如「A 在位置 5 插入了『你好』」、「B 刪除了位置 12 的字元」。這種做法頻寬效率極高,因為一次按鍵只產生幾十位元組的資料。但它要求所有操作必須按照因果順序送達,而且每一台裝置都必須能正確地「重放」這些操作。一旦順序錯亂或重複送達,結果就會不一致,因此需要額外的排序與去重機制。
以狀態為中心的思路則是:直接傳「目前長什麼樣子」,或者傳「狀態的差異摘要」。這種做法比較寬容,因為狀態本身是自我描述的,接收方可以用任何順序合併,只要合併函數滿足交換律、結合律與幂等性即可。代價是資料量較大,且需要設計一個可以合併的狀態表示法。
有趣的是,現代主流的協作演算法其實常常是「以操作為形式、以狀態收斂為目標」的混血設計。例如 Yjs 內部的更新(update)是一種二進位編碼的操作集合,但這些更新本身具備可合併性,因此可以在任意順序下套用而不會產生分歧。這種設計同時取得了頻寬效率與收斂保證,是目前相當受歡迎的路線。
三、衝突解決的兩大主流路線:OT 與 CRDT
談到協作同步的原理,絕對避不開這兩個縮寫:OT(Operational Transformation,營運轉換)與 CRDT(Conflict-free Replicated Data Type,無衝突複製資料型別)。它們是目前幾乎所有即時共編產品的技術基礎。理解它們的差異,你就會明白為什麼有些產品在同時編輯時流暢得像魔術,有些卻會出現文字亂跳、重複段落、甚至整段消失的災難。
3-1 營運轉換(OT):先排隊,再換算
OT 的核心思想可以用一句話概括:把並發的操作轉換成在彼此之後執行的等效操作。
想像一個最簡單的場景。文件內容是「ABC」。使用者甲在第 1 個位置插入「X」,得到「XABC」;使用者乙同時在第 3 個位置插入「Y」,他基於原始內容「ABC」計算,認為位置 3 是 C 之後,所以得到「ABCY」。現在伺服器要合併這兩個操作。
如果直接把甲的「在位置 1 插入 X」套用到乙的結果上,會得到「XABCY」,看起來沒問題。但如果先套用乙的操作再套用甲的操作呢?乙的操作是「在位置 3 插入 Y」,在「XABC」上執行,位置 3 會落在 B 與 C 之間,結果變成「XABYC」,與「XABCY」不一致。這就是衝突。
OT 的解法是設計一個轉換函數 transform(opA, opB),把甲的操作相對於乙的操作進行「位移修正」。在這個例子裡,因為乙的插入發生在甲插入位置的右邊,所以甲的操作不需要調整;但反過來,乙的操作需要被修正為「在位置 4 插入 Y」,才能在甲已經插入 X 的文件上得到正確結果。經過轉換後,兩條路徑都會收斂到「XABCY」。
在只有兩個使用者的情況下,這種轉換還算直觀。但真實場景有 N 個使用者、每個使用者可能連續產生多個操作,而且網路延遲導致操作到達伺服器的順序亂七八糟。這時 OT 需要一個中央伺服器作為「序列化權威」:所有操作先送到伺服器,伺服器維護一份權威文件狀態與操作歷史,並對每一個新到的操作,根據它相對於歷史操作的關係進行一連串轉換,然後廣播給所有客戶端。
OT 的優點是歷史悠久、理解相對直觀、在文字編輯場景有大量成熟實作(例如 Google Docs 早期使用的 Jupiter 協定、Etherpad 的作法)。缺點也很明顯:轉換函數的設計與證明極其困難,尤其在多維度操作(同時涉及插入、刪除、格式變更)時,很容易出現無法收斂的邊角案例;此外,它高度依賴中央伺服器,對於離線優先的架構並不友善——因為離線期間產生的操作無法即時與伺服器序列化, reconnect 時需要進行大量的歷史轉換,複雜度會快速膨脹。
3-2 無衝突複製資料型別(CRDT):讓合併天生可交換
CRDT 走的是另一條路。它的核心思想是:不要試圖「轉換」操作,而是設計一種資料結構,使得任何順序的合併都會得到相同結果。
要達到這個目標,合併函數必須滿足三個數學性質:交換律(先合併 A 再合併 B,與先 B 再 A 的結果相同)、結合律(分組不影響結果)、幂等性(重複合併同一份資料不會改變結果)。只要滿足這三點,網路封包的順序、重複、延遲就都不再是問題,因為最終結果只取決於「哪些操作被看到」,而不取決於「看到的順序」。
CRDT 又可以分成兩大類。狀態型 CRDT(state-based / CvRDT)是直接合併狀態,例如一個「計數器」可以表示成一個向量,記錄每個節點各自增加了多少次,合併時取各維度的最大值或總和。操作型 CRDT(operation-based / CmRDT)則是傳遞操作,但要求操作本身具備可交換性,且需要可靠的通訊保證(例如使用因果廣播)。
在文字編輯場景裡,最關鍵的是「序列 CRDT」。它要解決的問題是:兩個人在同一段落的不同位置插入文字,如何決定最終順序?如果沒有額外資訊,「同時插入」的先後是無法判斷的。因此序列 CRDT 會給每個字元一個唯一的識別碼,並在識別碼中編碼某種順序關係(例如 Lamport 時間戳加上節點 ID)。當兩個字元被插入到同一個位置時,系統就用識別碼的大小來決定先後,這個規則在所有節點上都是一致的,因此結果必然收斂。
常見的序列 CRDT 演算法包括 Treedoc、Logoot、LSEQ、RGA(Replicated Growable Array)以及 Yjs 使用的 YATA。它們的差異主要在於識別碼的編碼方式、插入時需要攜帶的上下文資訊多寡,以及墓碑(tombstone)清理的效率。以 YATA 為例,它在插入時會記錄「左鄰居」與「右鄰居」的識別碼,並在合併時根據這些鄰居關係重建順序,因此可以處理巢狀結構與複雜的並發插入。
CRDT 的最大優勢是天然支援離線與點對點架構。因為合併不依賴中央伺服器的排序,所以每一台裝置都可以獨立編輯,等到有機會連線時再進行合併。這正是「本地優先(local-first)」軟體運動的技術基礎。代價則是中繼資料會膨脹:每個字元都要帶識別碼,刪除後還要保留墓碑以維持順序資訊,長期下來文件大小會比純文字大上好幾倍,因此需要設計垃圾回收與壓縮機制。
3-3 OT 與 CRDT 的比較與選型建議
下表整理兩者在幾個關鍵維度上的差異,方便你在評測或選型時快速對照。
比較維度
OT(營運轉換)
CRDT(無衝突複製型別)
架構依賴
通常需要中央伺服器作為序列化權威
可去中心化,支援點對點與離線
離線支援
較困難,重連時需大量歷史轉換
天生支援,合併順序無關
中繼資料負擔
較小,僅需保存操作歷史
較大,需字元識別碼與墓碑
實作與驗證難度
轉換函數的邊角案例極多,證明困難
數學性質明確,但資料結構設計複雜
語意保真度
可針對特定操作做語意轉換
偏向以結構規則保證收斂,語意較弱
代表實作
Google Docs 早期、Etherpad、ShareDB
Yjs、Automerge、Diamond Types
選型上沒有絕對答案,但可以依照產品定位來判斷。如果你的產品是純雲端、永遠在線、以文字編輯為主,且團隊有能力維護中央伺服器,OT 是一條成熟穩定的路徑。如果你強調離線優先、跨裝置同步、點對點協作,或者需要處理結構化資料(例如巢狀區塊、圖結構),CRDT 會是更合適的選擇。近年來,隨著 Yjs、Automerge 等函式庫成熟,CRDT 的實作門檻已大幅降低,這也是為什麼新創筆記軟體幾乎一面倒傾向 CRDT 的原因之一。
四、從原理到工程:傳輸層、離線處理與一致性模型
演算法只是骨架,真正讓協作同步「動起來」的是工程細節。這一章要谈的是資料怎麼在網路上跑、斷線時發生什麼事,以及我們怎麼判斷兩份資料「算不算一致」。
4-1 傳輸層:WebSocket、輪詢與房間廣播
即時協作的第一個工程問題是:資料用什麼通道傳?最常見的答案是 WebSocket。它是一種在單一 TCP 連線上建立全雙工通訊的協定,連線建立後,伺服器可以主動推送訊息給客戶端,客戶端也可以隨時發送更新。對比傳統的 HTTP 輪詢(客戶端每隔幾秒問一次「有沒有新資料」),WebSocket 的延遲低、額外開銷小,是即時共編的標準選擇。
不過 WebSocket 並不是萬靈丹。它需要伺服器維持大量長連線,對記憶體與連線數管理是一大考驗;在行動網路切換、代理伺服器、企業防火牆的環境下,長連線也容易被打斷。因此實務上通常會設計降級策略:優先使用 WebSocket,若連不上則退回 Server-Sent Events(單向推送)或短輪詢。有些產品甚至會在區網內嘗試 WebRTC 直連,以降低延遲。
連線建立之後,下一個問題是「誰該收到什麼」。多數系統會用「房間(room)」或「頻道(channel)」的概念來組織廣播範圍:一份文件的編輯者加入同一個房間,伺服器只把更新推送給該房間的成員。這樣可以避免無關的資料傳輸,也方便做權限控管——畢竟不是每個人都該看到每一份文件。
還有一個常被忽略的細節:訊息格式與批次處理。如果每一次按鍵都單獨送一個封包,在快速打字時會產生大量小封包,造成網路負擔。因此成熟的實作會做「批次合併」,把幾十毫秒內的多個操作打包成一個更新,再一次性送出。這需要在延遲與頻寬之間取得平衡,通常 30 到 100 毫秒是常見的取捨區間。
4-2 離線優先與最終一致性
「離線優先(local-first)」是近幾年在軟體架構圈很紅的概念,核心主張是:本機資料是主要副本,雲端只是同步與備份的媒介。使用者在沒有網路時,仍然可以完整地讀寫,所有操作先寫入本機資料庫,等連線恢復後再與伺服器或其他裝置合併。
這種架構對筆記軟體特別有意義,因為筆記的使用情境極度碎片化:你可能在飛機上寫會議紀錄、在山區健行時記靈感、在地下室停車場補一段待辦事項。如果每次沒網路就只能唯讀,體驗會非常斷裂。而 CRDT 正好提供了實現離線優先的數學基礎。
但離線優先也帶來一個必須面對的現實:最終一致性(eventual consistency)。系統不保證任何時刻所有裝置看到的內容都相同,只保證「在沒有新寫入的情況下,經過足夠時間,所有裝置會收斂到相同狀態」。這段「收斂前的空窗期」就是使用者可能看到暫時不一致的時刻。例如你在手機上改了標題,但桌機還沒收到更新,這時桌機顯示的還是舊標題。多數產品會用「同步中」的視覺提示來安撫使用者,但根本上這是架構決定的特性,不是 bug。
另一個頭痛的問題是墓碑累積與儲存膨脹。CRDT 為了維持順序資訊,刪除字元後不能真的移除,而要留下「這裡曾經有一個字元,但它被刪了」的墓碑。長期編輯的文件,墓碑數量可能遠超過實際內容。因此工程上需要設計壓縮機制,例如在確認所有裝置都已同步後,安全地清除舊墓碑,或者定期產生「快照」來取代完整的操作歷史。
4-3 版本向量與因果關係
要判斷兩份資料「誰新誰舊」或「是否已經合併」,需要一套時間表示法。在分散式系統裡,實體時鐘不可靠(每台裝置的時鐘都可能不準),因此學界提出了邏輯時鐘的概念。最常見的是 Lamport 時間戳與向量時鐘(vector clock)。
向量時鐘的作法是:每個節點維護一個向量,記錄「我知道每個節點各自發生了多少次事件」。當節點 A 產生新事件時,就把自己的計數加一;當收到別人的訊息時,就把對方的向量與自己的向量取逐維最大值。透過比較兩個向量,可以判斷出三種關係:A 是 B 的祖先(B 較新)、B 是 A 的祖先(A 較新)、或者兩者互不為祖先(代表是並發事件,需要合併)。
這個機制在協作同步裡非常重要,因為它讓系統能夠判斷「這個更新我是不是已經看過了」,從而實現幂等處理,避免重複套用。它也是離線合併的基礎:當一台裝置重新連線,它可以帶著自己的向量時鐘,與伺服器比對,只請求缺少的那部分更新,而不必傳輸整份文件。
不過向量時鐘的成本會隨節點數量線性成長。對於一份只有三五個人編輯的文件,這沒什麼問題;但如果是一份上千人共筆的文件(例如大型組織的知識庫),向量就會膨脹到不可忽視。因此有些系統會改用「版本號加裝置 ID」的簡化方案,或者定期做垃圾回收,把不再活躍的節點從向量中移除。
五、評測視角:怎麼判斷一款筆記軟體的同步做得好不好
理解了原理之後,我們可以把它轉換成可操作的評測方法。以下這份清單,是你在試用一款筆記軟體時,可以主動驗證的項目。它們不需要你懂程式碼,只需要一點耐心與兩台裝置。
5-1 實測清單:十個可自行驗證的場景
第一,同時編輯同一段落。請兩位使用者同時在同一段落的不同位置打字,觀察文字是否會錯位、重複或消失。好的實作會讓兩邊的輸入自然交錯,壞的實作會出現「文字跳來跳去」或「其中一方打的字被吃掉」。
第二,同時刪除與插入。一人刪除一段文字,另一人同時在該段落中間插入新字。觀察合併結果是否合理。這是 OT 與 CRDT 都容易出錯的邊角案例。
第三,離線編輯後重連。請其中一人關閉網路,持續編輯五到十分鐘,然後恢復連線。觀察內容是否完整保留、位置是否正確。這是檢驗產品是否真正支援離線優先的關鍵測試。
第四,雙向離線衝突。兩人同時斷網,各自修改同一段落,然後同時恢復連線。觀察系統如何處理這種「雙方都改了同一段」的情況。好的系統會合併雙方內容,差的系統會出現覆蓋或重複。
第五,大量貼上。一次貼上數千字的內容,觀察同步延遲與是否出現內容截斷。這考驗的是批次處理與資料壓縮的效率。
第六,結構化內容測試。同時對同一份清單調整順序、對同一張表格插入列、對同一個區塊改變類型(例如把段落變成標題)。觀察結構是否會錯亂。
第七,版本歷史回溯。編輯一段時間後,嘗試還原到某個歷史版本,觀察還原後其他協作者的畫面是否同步更新。
第八,權限變更。把某位成員從「可編輯」改為「唯讀」,觀察他的畫面是否即時反映、正在編輯的內容是否被妥善處理。
第九,多裝置同一帳號。在手機、平板、電腦同時登入同一帳號並編輯,觀察三邊是否收斂。這可以測出系統是否正確處理「同一使用者、多個客戶端」的並發。
第十,長時間掛機。讓一個頁面開著數小時不動,觀察回來後是否仍能正常同步,還是需要重新整理。這考驗連線保活與斷線重連的處理。
5-2 常見的同步問題與症狀對照
當同步出問題時,症狀通常會透露病因。以下是一些常見的對照,幫助你在遇到狀況時快速判斷。
愈用愈慢:中繼資料或墓碑沒有被清理,文件內部結構隨編輯次數膨脹。
這些症狀背後對應的,都是我們前面談過的原理缺口。理解原理的好處,就是你不需要等官方公告,也能大概猜出問題出在哪一層,並決定是要回報 bug、調整工作流程,還是直接換一套工具。
六、結語:協作同步是一種產品哲學
回顧整篇文章,我們從產品需求談到資料模型,再拆解 OT 與 CRDT 的運作機制,最後落到傳輸層、離線處理與實測方法。你會發現,協作同步從來不是單一技術問題,而是一連串取捨的總和:要精細還是要簡單、要即時還是要離線、要中央權威還是要去中心化、要語意保真還是要收斂保證。
對使用者而言,這些取捨最終會反映在體驗上:一份文件能不能讓五個人同時安心編輯、能不能在飛機上繼續寫、能不能在半年後準確還原某個版本。對產品團隊而言,這些取捨則決定了技術債的形狀:選了 OT,就要養一個強大的伺服器團隊與轉換函數的測試矩陣;選了 CRDT,就要面對中繼資料膨脹與壓縮策略的長期抗戰。
所以,當你下次打開一款筆記軟體,看到兩個游標在同一段落裡和諧地交錯,不妨多想一秒:這背後是幾十年的分散式系統研究、無數次的演算法證明、以及工程團隊對延遲與頻寬的斤斤計較。協作同步之所以值得被認真對待,不只是因為它很酷,而是因為它讓「共同思考」這件事,第一次有了幾乎沒有摩擦的數位載體。而這,正是現代筆記軟體最迷人的地方。