後端系統設計 - 設計即時共編文件系統 (Collaborative Editor)

2024年3月28日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

本篇詳細解說本收錄在 E+ 的後端系統設計專題

從後端的角度來看,當今天收到一個「即時共編文件」的需求,最先會想到的是什麼呢?

我們試著從最簡單的情境開始思考。假如是一個古早時期的 Word 文件,只需要在編輯後,當使用者按下存檔按鈕後,把新增、修改、刪除存在本地即可。在這種情況下,我們要思考編輯器要怎麼設計、資料該怎麼存,要處理的問題相對單純一些。

然而進一步往下想,如果今天是要讓這個文件雲端化,但是先不支援即時共編,那會變成怎麼樣呢? 這種狀況要做的事情比較多一點。因為雲端化,牽涉的就不只是本地端,而是要牽涉到雲端,這時勢必要考量到遠端的資料怎麼存、怎麼拿,以及因為要跟雲端拿資料,所以會有延遲性的問題要最佳化;此外,當流量大之後,要面對處理規模化的問題。

而如果要進一步做到即時共編,就會有更進一步的難題要處理,讓我們在下個段落詳細討論。

即時共編的困難點在哪

即時共編有兩個主要的技術難點需要處理。第一個是如何做到即時,第二個是共編時遇到的衝突問題。讓我們分別來討論這兩個問題,讓大家有比較具體的理解。

首先考慮一個情況,假如今天的文件中有兩個段落,段落 A 跟段落 B,而用戶一把段落 A 的內容從 Explain 改成 ExplainThis,而用戶二把段落 B 的內容從 Explain 改成 ExplainThat,這種情況下,因為是不同的兩個段落,所以沒有衝突要解決,只是仍需要處理即時的問題。

大家可以用平常在寫程式的 Git 來理解。今天兩個人同時在程式碼加上新內容,因為是不同段落,所以推 (push) 後再拉 (pull),Git 不會有衝突要解。但因為是非即時的,所以兩邊的內容如果要一致,會需要 A 把 ExplainThis 的改動推上去,B 把它拉下來,然後 B 把 ExplainThat 的內容改動推上去,然後 A 拉下來,這樣兩邊才會一致。

如何讓系統可以「即時」做到這件事,不用手動推拉,是第一個要解決的問題。

然而,如果只解決即時,在一個即時共編系統還不夠,因為共編意味著可能會有衝突。就像用 Git 時,如果改不同段落不用解衝突,但如果改了同一行內容,一推一拉下,就會有衝突需要解

以上面的例子來說,當今天用戶一把段落 A 的內容從 Explain 改成 ExplainThis,而用戶二同時把段落 A 的內容從 Explain 改成 ExplainThat。因為兩邊都是改段落 A,當 A 推完後,B 拉下 A 推的內容,就會有衝突需要解。

在用 Git 的時候,我們會手動去解衝突。但試想,一般使用 Google Doc 或 Notion 的使用者,如果跟別人共編每幾分鐘就要這樣手動解衝突,使用體驗肯定不會好。因此需要有方法來解決共編的衝突。

在描述完兩個難題後,讓我們分別來討論如何解決吧。

如何解決即時性 (real-time) 的問題?

讓我們先來討論即時性的問題。所謂的即時性,是指當今天用戶一做了更新,該更新會直接出現在用戶二正在共編的文件上。同樣地,如果用戶二做了更新,改動也會出現在用戶一的文件上。

要如何實踐即時性呢? 一個直觀的想法,是今天客戶端定期向伺服器端發送請求。用 Git 的例子來比喻,原本都是要同步時,才手動去拉遠端的內容;像要做到即時收到更新,可以寫程式,每隔幾秒就去拉遠端的內容。顯然這做法不是太好,因為如果遠端沒有更新,你的程式還一直去拉,那就等同於浪費請求。

上面這種做法,有個名字叫做輪詢 (polling)。可以試著想一想,要如何避免輪詢會造成的浪費? 很簡單,我們可以發一個請求後,伺服器端先跟客戶端保持連接,直到伺服器端有更新後,再回傳更新的內容。這樣一來就不會有重複發送浪費請求的問題。

這種保持長連接,直到有更新後伺服器才會回傳的做法,叫做長輪詢 (long polling)。雖然說長輪詢能減去輪詢的浪費問題,但仍有其限制所在。舉例來說,長輪詢仍然會有時間所限制的 timeout 問題,所以仍是可能有重複請求的問題。而重複請求意味著每一次都有請求要處理的驗證等問題,是相對消耗成本的。

這時你可能會思考,在客戶與伺服器端的模式中,都是客戶端主動發送請求,伺服器端收到請求後才回應。因為在這種限制下,我們只能用長輪詢這類方法。但是,有沒有可能反過來,不是客戶端發送請求後,伺服器端才能回應;而是伺服器端主動就可以傳送內容給客戶端?

如果你有這種跳脫原本框架的思維,那就是突破的開始。而在目前的業界,也確實有這種做法。具體來說,業界目前主流使用 WebSocket 這個雙向協定。因為該協定是雙向的,意味著可以從伺服器端主動傳送更新到伺服器端。

以及時共編文件來說,只要其他使用者有更新,伺服器端就會把更新,透過 WebSocket 的接口,傳送給其他的客戶端。先前在《後端系統設計 - 設計聊天系統 (Chat System)》我們有針對 WebSocket 的各種細節作討論,推薦大家可以看那篇了解更多。

從歷史的角度來看,早些年業界確實會使用長輪詢。舉例來說,在 這篇 FB 官方技術文 提到,Facebook 在 2010 年時,還是用長輪詢的方式,當時的時代背景是 WebSocket 技術還沒有很成熟,所以那時的 Facebook 也才剛開始研究 WebSocket。是一直到了 2012 年底,Facebook 正式開始在生產環境用 WebSocket (詳見這篇 FB 官方技術文)。

本文為 E+ 成長計畫的深度內容,截取前三分之一開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們