DDIA 导读(四):编码与演化
2026/9/13 20:57:33 网站建设 项目流程

本文是《Designing Data-Intensive Applications》(DDIA,中文译名《数据密集型应用系统设计》)第 4 章的导读。DDIA 是 Martin Kleppmann 所著的分布式系统经典,本系列逐章导读,把书的核心概念讲清楚。

一句话主旨

数据比代码活得久。应用程序会持续演进、持续发布,但已经写出去的数据不会跟着升级——所以数据的编码格式必须能容许 schema 变化,而容许变化靠的是"向前兼容"和"向后兼容"这两个纪律。这一章讲的是:如何让数据在时间维度上"活下来"。


核心概念拆解

1. 编码格式——数据怎么变成字节

数据在内存里是对象/结构体(指针、引用、嵌套),写到磁盘或网络时要编码(serialize)成字节序列,读回来要解码(deserialize)。编码格式的核心选择:

磁盘/网络

内存中的数据结构

编码 encode

解码 decode

对象/结构体
有指针、嵌套引用

字节序列
连续字节

三大类编码格式

格式类型人类可读Schema演化支持
JSON / XML文本无/可选弱(类型模糊、无 schema 约束)
Protobuf / Thrift二进制必须有强(字段编号/optional)
Avro二进制必须有强(schema 注册表,读写比对)

2. 语言专属格式——别用

Java/Python 各有自带的序列化(pickle、Java Serializable),DDIA 明确警告:不要用语言专属格式做持久化或跨服务通信。原因:绑死语言、跨语言不可读、演化能力差、常有安全漏洞(pickle 反序列化能执行任意代码)。选 JSON、Avro、Protobuf 等跨语言格式。

3. JSON/XML 的问题

JSON 简单通用,但有几个演化上的痛点:

  • 类型精度不足:数字与字符串可区分,但整数与浮点不区分、无法指定精度,且无 schema 约束,解码后类型细节靠猜
  • 没有 schema 约束:加字段/减字段无人挡,接收方要容错处理"字段可能不存在"
  • 嵌套和数组表达力弱:JSON 能存嵌套数组,但查询语言对"数组里某对象的某字段"的访问能力有限
  • 冗余:每条记录都重复字段名,空间浪费

4. Protobuf / Thrift——带 Schema 的二进制

核心思想:用一个独立的 schema 文件(.proto)描述数据结构,编解码都依赖它。字段用编号标识,不靠字段名。

message Document { int32 id = 1; // 字段编号1, 名字id string dataset = 2; // 字段编号2 float score = 3; // 字段编号3 optional string title = 4; // 新加的字段, 旧数据没有 }

为什么用编号不用名字:编号是字节里的标识,编号不变就不会破坏兼容。改名(id → doc_id)无所谓,只要编号 1 不变。加新字段给个新编号,旧代码读到不认识的编号直接跳过(向前兼容);旧数据没有新字段,新代码按 optional/默认值处理(向后兼容)。

演化的纪律

  • ✅ 加新字段(给新编号)
  • ✅ 删字段(但编号不能复用)
  • ✅ 字段改 optional
  • ❌ 改字段类型(int → string 破坏兼容)
  • ❌ 复用已删字段的编号(旧数据里那个编号还是旧含义)

5. Avro——为大数据而生,schema 动态比对

Avro(Hadoop 生态)有个独特设计:schema 写在数据文件头里,每条记录不带字段名/编号,纯值

Avro 文件

文件头: schema
(字段名+类型)

记录1: 纯值 0.8,stem,...

记录2: 纯值 0.5,industry,...

关键机制——读写 schema 比对

  • 写数据时用 writer’s schema(存进文件头)
  • 读数据时用 reader’s schema(当前程序的 schema)
  • Avro 运行时比对两个 schema,按字段名匹配,处理差异

举例:旧数据用 writer’s schema(有score字段,无category字段),新程序用 reader’s schema(有scorecategory两个字段)。Avro 按字段名匹配:

  • score:读写都有 → 正常读
  • category:读有写无(新程序期望 category 但旧数据没存)→ 用 reader’s schema 里category的默认值,没有默认值则报错(Avro 不隐式填 null)
  • 反过来,如果旧数据有legacy_tag但新 schema 删了它:写有读无 → 读取时丢弃,不报错

为什么适合大数据:记录不带元数据,极紧凑;schema 演化靠字段名匹配(不是编号),加字段只要给默认值即可。Hadoop/Spark 生态大量用 Avro/Parquet 正因如此(Parquet 是另一种独立的列式格式,设计动机不同)。

Protobuf vs Avro——核心差别

两者都解决同一个问题(带 schema 的紧凑二进制 + 演化兼容),但诞生于不同生态,设计取向不同:

ProtobufAvro
字段标识编号(编号不变就兼容)字段名(按名匹配)
schema 位置独立 .proto 文件,编解码双方各自持有写进数据文件头,读时与 reader’s schema 比对
记录冗余每条记录带字段编号(紧凑但不零冗余)每条记录纯值,零元数据冗余(最紧凑)
适合场景服务间通信(gRPC)、跨语言 API大数据文件存储(Parquet/Avro 文件)、批处理管道
演化方式加字段给新编号,旧代码跳过未知编号加字段给默认值,读写 schema 动态比对

如果你的工作涉及服务间通信(微服务 API、gRPC),Protobuf 是主流选择;如果涉及大数据存储格式(Spark/Hadoop 管道里的 Parquet/Avro 文件),Avro 更常见。

6. 向前兼容 / 向后兼容——两个方向

这是全章最该记住的概念:

新版本加optional字段
旧数据没该字段, 用默认值

旧代码遇到新字段
必须能跳过不崩

向后兼容 Backward Compatible
新代码能读旧数据

向前兼容 Forward Compatible
旧代码能读新数据

✅ 容易做到

⚠️ 需要纪律

  • 向后兼容(新读旧):通常容易,新代码认识旧格式即可。
  • 向前兼容(旧读新):更难,要求旧代码遇到不认识的新字段/新格式时不崩溃、能跳过。这需要编码格式本身支持"忽略未知字段"。

双方向都需要的场景:分布式系统里多个节点版本不一致时(滚动升级期间,新旧节点并存),必须同时满足向前和向后兼容。

7. 数据流的演化模式

DDIA 把数据流转分三种模式,每种演化的挑战不同:

数据流典型演化挑战
数据库写入后读向前+向后都要(滚动升级时新旧版本并存,新代码写新格式旧代码读要兼容)
RPC/REST服务间调用向前+向后都要,服务端客户端独立部署
异步消息Kafka/RabbitMQ最难,消息可能堆积,生产者已升级消费者还是旧版

消息队列的演化风险高于数据库,因为消息可能在队列里躺很久,跨越多个版本周期。如果消息格式(body 的字段)要演进,生产者发新格式、消费者还没升级——这就是向前兼容问题。选 JSON body 要靠"消费者忽略未知字段"的人工纪律;选 Protobuf/Avro 则有格式级保障。

8. Schema-on-Write vs Schema-on-Read(演化视角,跨章回顾)

第 2 章提过,第 4 章从演化角度再讲:

  • Schema-on-Write(如 MySQL/PostgreSQL):写时强制校验 schema。加列要ALTER TABLE,旧数据该列填默认值。演化有明确纪律,但迁移要停服或在线 DDL。
  • Schema-on-Read(如 JSON 存半结构化列、文档引擎宽松 mapping):不强制校验,字段有无都容错。演化灵活(加字段不用改表),但数据质量靠应用自律

Schema-on-Read 的演化自由是用查询表达力和数据质量保障换的——加字段零成本,但查询层无法精确依赖字段结构,数据可能脏。这是第 2 章 + 第 4 章共同揭示的权衡。

问题→方案:问题——数据比代码活得久,schema 要变但旧数据不能重写。场景——应用滚动升级期间新旧版本并存,新代码加字段、删字段、改结构。方案——编码格式支持向前/向后兼容:Protobuf 靠字段编号(编号不变就兼容),Avro 靠读写 schema 动态比对(按字段名匹配、缺字段填默认值),JSON 靠"接收方忽略未知字段"的人工纪律。纪律越严(Protobuf/Avro),演化越可控;自由度越高(JSON 无 schema),演化越靠自觉。


Mermaid:数据流演化模式

数据库流
只需向后兼容

RPC/REST流
向前+向后都要

异步消息流
最难: 消息可能跨越多版本

加列ALTER, 旧数据填默认值

服务端客户端独立部署
要双向兼容

消息堆积期间
生产者新格式消费者旧版


下一篇:第 5 章——复制。进入全书最难的第二部分(分布式数据),从"单机怎么存"转向"多机怎么保持一致"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询