DDIA 導讀 CH5 — 編碼與演化 (上)

2026年6月28日

💎 加入 E+ 會員方案 與超過千位工程師一同在社群成長,並獲得更多深度的軟體前後端學習資源

在 DDIA 的第五章,作者拆解與比較了業界常見的資料編碼格式,像是 JSON、XML、Protocol Buffers 和 Avro。作者不僅討論這些不同的格式如何表示資料,還討論了在資料格式演進時,如何處理相容性問題。

在實務上,系統不可能每次改版都一次更新所有服務、所有資料、所有使用者端。因此,新舊版本的程式碼和資料往往需要共存。這時候,一個資料格式能不能支援資料結構規格 (schema) 的變更,就會變得很關鍵。

在章節的後半段,作者也進一步討論,這些格式在資料儲存和系統通訊中的用途,包含資料庫、Web 服務、REST API、RPC、工作流程引擎,以及訊息佇列這類事件驅動系統。

編碼資料格式

在寫程式時,通常至少會有兩種資料格式,一種是在記憶體中的資料結構 (例如陣列、樹等結構),這類格式會針對 CPU 的操作做最佳化。但是如果要透過網路傳資料,不能直接把記憶體中的東西傳出去,因為記憶體位置只對當下的程式有意義,如果換到另一台機器或者另一個程序 (process),記憶體位置的指標就完全沒意義。

因此,如果要透過網路傳送資料,就需要對資料進行編碼 (encode),把資料轉換成能夠被存進檔案、在網路上傳遞、到別台機器後也能被讀懂的格式。舉例來說,JSON 就是最廣泛被使用的格式之一。

因為對大型系統來說,資料幾乎不會只待在一段程式中,資料會被寫入資料庫,會被透過 API 從後端傳到前端。因此,編碼 (把程式中好操作的資料結構,轉成其他機器與其他端也能理解的格式),就會是資料密集系統中的重要課題。

因此,當我們在看 JSON、XML,或者 Protocol Buffers 時,本質上都在面對「當資料要被傳輸時,我們該如何轉成其他人能安全、穩定,又有效率讀懂的格式」這個問題。

JSON、XML、二進位制變體

當談到標準化編碼,JSON、XML 幾乎是大家第一時間會想到的,因為幾乎所有程式語言都有成熟的解析與產生工具 (例如 Java 有 java.io.Serializable,Python 有 pickle,Ruby 有 Marshal)。因此,即使今天用 Java 寫後端、JavaScript 寫前端,同時用 Python 做資料處理,只要每一端都約定好用 JSON,資料就可以互相傳輸。

然而,在使用 JSON 與 XML 時,其實會遇到一些不方便的地方。

第一點是數字的編碼。平常在寫資料時,可能覺得數字就是數字,字串就是字串,但在 XML 中,<id>12345</id> 其實無法讓人判斷這是數字還是字串,需要搭配額外的結構規格才行。同樣地,在用 JSON 時,雖然能區分數字跟字串,但是在數字中,沒辦法進一步區分整數和浮點數,也沒有規定精度。

這在某些場景下相當危險,如果一個系統用 64-bit 整數當 ID,但把這個數字編碼成 JSON 數字傳給 JavaScript,被解析後可能會失去精度。因此作者有提到,Twitter 用 64-bit 數字來識別貼文,但為了避免 JavaScript 應用解析錯誤,在 API 回傳時,會有兩個 id,一個是 JSON 數字,另一個是十進位字串。

第二個困擾,是 JSON 跟 XML 沒有原生支援二進位資料。換句話說,如果是傳圖片、音檔等加密後的檔案內容,因為是原始的二進位位元組 (bytes),所以往往需要額外轉換。常見的做法是轉成 Base64 字串。但這一來很繞,明明要傳位元組,卻要先轉成字串。二來這樣轉換後,往往大小會增加,如果檔案比較大,空間使用的成本就變大。

第三個文中談到的問題是沒有足夠強的資料規範。舉例來說,單純看 JSON,沒辦法判斷資料長成什麼樣,因此需要額外搭配 JSON Schema,但這會讓使用上變複雜。以 JSON 來說,schema 支援開放內容與封閉內容兩種模型。假如像下面這樣定義了 userName

{
  "userName": "string"
}

在開放內容模型中,下面這樣會是被允許的。這意味著,JSON Schema 並不是在定義哪些欄位可以存在與否,而是如果有某個欄位被定義,就需要按照規則;但即使沒被定義也可以出現,只是不會被特別規範。這種特性的好處是,系統演進上會變得比較容易,如果新版的服務要加欄位,可以輕易地相容。只是反面來說,會讓人無法光看 schema 就精準判斷資料的內容有哪些。

{
  "userName": "Martin",
  "age": 30
}

進一步說,JSON Schema 的開放模式,能夠透過 patternProperties 來定義欄位。底下是來自書中的範例,其中就定義了所有鍵 (key) 必須符合正規表示式 ^[0-9]+$,也就是只能由數字組成,同時對應的值 (value) 必須是字串 (string)。這種欄位的定義可以讓定義做到非常細緻的限制,但與此同時會增加管理的複雜度。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "patternProperties": {
    "^[0-9]+$": {
      "type": "string"
    }
  },
  "additionalProperties": false
}

由於 JSON 跟 XML 的諸多問題,且佔用的空間不小,在業界有人提出用二進位的方式 (binary encoding),來做到 JSON 或 XML 能做的事。以書中的例子來說 (見下方重製的圖),假如同樣是 userName: "Martin",在這種表達下,可以用 0xa8 來表達 userName 以及 0xa6 來表達 "Martin"。相比之下,這種方式佔用比較小的空間。

然而,這種做法在社群中沒有受到廣泛的採用。最主要是因為對多數人類開發者來說,可讀性不佳。在許多場景中,多佔用一點空間的影響不大,但如果可讀性不佳,對於跨團隊合作 (例如前後端、不同微服務的團隊) 帶來極大的溝通成本,總體是弊大於利。

Protocol Buffers

對比起二進位的編碼,Google 推出的 Protocol Buffers (以及 Facebook 開發的 Thrift),則同樣能用比較小的空間表達資料,這兩種編碼的特色在於,實際傳輸的資料仍然是二進位格式,但資料結構會透過 .proto 或 IDL 明確定義,因此開發者閱讀 schema 時,仍然能理解資料長相與欄位意義。相比之下,可讀性上沒被犧牲,因此在社群中,成為更多人選擇的格式。

具體來說,Protocol Buffers 是一種需要先定義資料格式的二進位序列化格式。它會把資料壓得比 JSON 更小。之所以能把資料壓更小,是因為有預先定義好,且需要嚴格遵守的 schema (不像 JSON 的開放模式那樣寬鬆)。

讓我們一起來看書中舉的例子。下面是一個用 JSON 表達的資料。在單筆資料下,這似乎沒什麼問題,但假如今天是一個列表,每筆資料都會出現 "userName""favoriteNumber",以及 "interests",當資料量一大,就會不必要地佔用非常多空間。在網路傳輸上,等於額外的浪費。

{
  "userName": "Martin",
  "favoriteNumber": 1337,
  "interests": ["daydreaming", "hacking"]
}

不過同樣的資料用 Protocol Buffers,會先透過 IDL (接口定義語言) 定義以下

message Person {
    string user_name = 1;
    int64 favorite_number = 2;
    repeated string interests = 3;
}

這等同於約定了 1 代表 user_name,2 代表 favorite_number,3 代表 interests。這樣之後資料裡只要放 1、2、3 就好,因此能大幅減少空間的佔用,對於儲存更加有效率。而對於傳輸來說,只要接收方也先收到這份定義,就能知道對應關係,這樣傳輸時也能大幅節省資料量。

然而,在這種設計下,要如何確保資料的欄位修改後,仍然可以相容?

Protocol Buffers 有針對向前與向後相容分別做處理。舉例來說,如果上述的 Person 新增了 string email = 4,舊程式收到的定義沒有 4,在 Protocol Buffers 的設計下,看到不認得的欄位會視為 unknown field 忽略,舊程式不會因為看到新欄位就解析失敗,藉此做到向前相容。與此同時,如果新程式讀到舊的資料,發現沒有 email 這個新的欄位,則是會給予預設值,透過這樣做到向後相容。

不過也要特別注意,假如有欄位要刪除,不能把該欄位拿去做其他用途。舉例來說,上面的 favorite_number 如果不用了,刪掉換成 string email = 2,可能會導致把舊的 favorite_number 誤解成 email。因此,假如有不用的欄位,通常刪除後會把編號保留,標記成 reserved 避免被誤認的狀況。

更改欄位也是,例如原本是 int32 favorite_number = 2,如果改成 int64 favorite_number = 2,好像看似只是把數字範圍變大,但可能造成新程式碼寫入一個大的數字,但是就程式碼還在用 int32 來讀,就可能發生截斷的狀況。所以在更改時,也需要特別注意各類極端案例。


加入 E+ 會員方案

對更深入了解這個主題,以及其他前後端開發、軟體工程、AI 工程主題感興趣的讀者,歡迎加入 E+ 一起成長 (連結)。

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