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 上追蹤我們