后端系统设计 - 设计即时共编文件系统 (Collaborative Editor)

2024年4月1日

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

本篇详细解说本收录在 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 上追蹤我們