3. 序列化与反序列化:JSON/Protobuf/Avro 对比、性能与兼容性

序列化这事儿,说白了就是把内存里的对象变成一堆字节,好让它能在网络上飞,或者存到磁盘里睡大觉。反序列化就是反过来,把字节再变回对象。

我这些年跟序列化框架打了太多交道了。从最早的 Java 原生序列化,到后来 JSON 一统天下,再到 Protobuf 和 Avro 在微服务和数据管道里大放异彩。每个框架都有自己的脾气,选错了,后面改起来真是欲哭无泪。

今天咱们就掰开揉碎,看看这三种主流方案到底该怎么选。

3.1 三大序列化框架速览

先给个全景图,让你心里有个数。

特性 JSON Protobuf Avro
数据格式 文本(自描述) 二进制(需 Schema) 二进制(Schema 内嵌或外置)
Schema 定义 无强制 Schema .proto 文件 .avsc (JSON 格式)
序列化速度 慢(文本解析) 快(二进制编码) 快(二进制编码)
数据体积 大(含字段名) 小(无字段名) 小(无字段名)
兼容性演进 灵活但易出错 强(有明确规则) 强(Schema 演化机制)
跨语言支持 极好 好 好
典型场景 Web API、配置文件 RPC、微服务通信 大数据管道、Kafka

嗯,这里要注意,JSON 虽然看起来最“自由”,但自由往往意味着责任。没有 Schema 约束,字段名改一下,新旧版本就全乱套了。

3.2 序列化性能:到底差多少?

我记得有一次做性能压测,系统瓶颈竟然卡在 JSON 序列化上。QPS 一上去,CPU 直接飙满。换成 Protobuf 后,同样的机器,吞吐量翻了一倍。

咱们看个简单的对比数据(基于 Java 环境,对象包含 10 个字段,100 万次序列化+反序列化):

指标 JSON (Jackson) Protobuf 3 Avro 1.11
序列化耗时 ~3200 ms ~450 ms ~520 ms
反序列化耗时 ~4100 ms ~380 ms ~490 ms
序列化后大小 ~180 bytes ~45 bytes ~48 bytes

看到了吧?JSON 的体积是 Protobuf 的 4 倍,速度慢了将近 8 倍。为什么?

JSON 是文本协议,每个字段名都要原样写进去。比如 "userName": "张三",光字段名就占了 10 个字节。Protobuf 和 Avro 用的是字段编号,比如 1: 张三,一个 Varint 就搞定了。

但话说回来,JSON 也有它的优势——调试方便。你直接 curl 一下就能看到返回结果,不用装额外的工具。所以我的习惯是:对外 API 用 JSON,内部 RPC 和数据管道用 Protobuf 或 Avro。

3.3 兼容性影响:这才是真正的坑

性能差几倍,顶多多花点钱买机器。但兼容性问题,搞不好就是线上事故。

我曾经遇到过这么一件事:一个老服务升级了字段类型,把 price 从 int 改成了 long,结果下游消费方反序列化直接抛异常。那晚的 on-call 电话,我现在还记得。

3.3.1 JSON 的兼容性:看似灵活,实则脆弱

JSON 没有 Schema,所以“兼容”全靠约定。常见的做法是:

  • 新增字段:下游要能忽略未知字段
  • 删除字段:上游要保证下游不再使用
  • 修改字段类型:基本是灾难,比如 "price": 100 改成 "price": "100",很多解析库会直接报错
避坑指南:我曾经见过一个团队,用 JSON 做微服务通信,字段名从 userId 改成了 user_id,结果 20 多个服务全部要跟着改。如果一开始就用 Protobuf 的字段编号,这种问题根本不会发生。

3.3.2 Protobuf 的兼容性:规则明确,但需遵守

Protobuf 的兼容性规则非常清晰,我总结成一句话:字段编号是契约,类型和编号都不能乱改。

具体规则:

  • 可以新增字段:用新的编号,旧代码会忽略
  • 可以删除字段:但 不能重用 被删除的编号(用 reserved 关键字标记)
  • 可以改字段名:但编号不变,序列化不受影响
  • 不能改字段类型:比如 int32 改成 int64,反序列化会截断或报错

看个例子:

// 旧版本 v1.proto
message Order {
  int32 id = 1;
  string product = 2;
  int32 quantity = 3;
}

// 新版本 v2.proto
message Order {
  int32 id = 1;
  string product = 2;
  // 删除了 quantity,但编号 3 必须保留
  reserved 3;
  // 新增字段,用新编号
  int32 amount = 4;
  string note = 5;
}

这里有个细节:reserved 3 是必须的。如果不写,将来有人不小心用了编号 3,新旧数据就会串,那 bug 极难排查。

3.3.3 Avro 的兼容性:Schema 演化是核心

Avro 的兼容性设计跟 Protobuf 不太一样。它把 Schema 本身作为数据的一部分(或者通过 Schema Registry 管理),反序列化时用 写 Schema 和 读 Schema 做对比,自动做字段映射。

Avro 定义了三种兼容性模式:

模式 说明 典型场景
BACKWARD 新 Schema 可以读旧数据(新增字段要有默认值) 消费者先升级
FORWARD 旧 Schema 可以读新数据(只能删除有默认值的字段) 生产者先升级
FULL 同时支持 BACKWARD 和 FORWARD 最推荐,但限制最多

举个例子,Avro Schema 演化:

// 旧 Schema (v1.avsc)
{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"}
  ]
}

// 新 Schema (v2.avsc) - 新增字段,必须给默认值
{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"},
    {"name": "email", "type": ["null", "string"], "default": null}
  ]
}

注意 email 字段的类型是 ["null", "string"],这是 Avro 的联合类型,表示可以为 null。默认值设为 null,这样旧数据反序列化时,新字段就能自动补 null。

我的建议:如果你用 Avro,强烈建议配合 Schema Registry 使用。这样所有 Schema 版本都有记录,兼容性检查可以在写入时就完成,而不是等到运行时才炸。

3.4 一张图看懂三种方案的演进逻辑

下面这张图,我画出了三种方案在数据演进时的核心差异。你看一眼就能明白,为什么 Avro 在数据管道里这么受欢迎。

三种序列化方案的数据演进逻辑对比 JSON 无 Schema 约束 字段名即契约 改字段名 = 破坏兼容 类型变更极易出错 全靠人工约定 ❌ 脆弱,不推荐用于长期演进 Protobuf 字段编号是契约 可新增/删除字段 删除字段必须 reserved 类型不可变 规则明确,工具检查 ✅ 强兼容,适合 RPC Avro Schema 演化引擎 读写 Schema 分离 自动字段映射 支持 BACKWARD/FORWARD 配合 Schema Registry ✅ 最强兼容,适合数据管道 核心结论 1. JSON:适合调试和对外接口,但不要用它做长期存储或跨服务核心通信。 2. Protobuf:字段编号是命根子,删字段必须 reserved,类型永远别改。 3. Avro:Schema 演化最灵活,但必须理解 BACKWARD/FORWARD 的区别。 4. 性能排序:Protobuf ≈ Avro >> JSON(体积和速度都是数量级差距)。 5. 我的经验:内部系统用 Protobuf,数据管道用 Avro,对外 API 用 JSON,各司其职。

3.5 实战选型建议

说了这么多,到底怎么选?我根据自己的经验,给几个场景化的建议:

  • 微服务 RPC 调用:首选 Protobuf。性能好,兼容规则明确,gRPC 生态成熟。
  • 大数据管道(Kafka、Hadoop):首选 Avro。Schema 演化能力强,配合 Schema Registry 可以做到无缝升级。
  • 对外 REST API:用 JSON。客户端语言千奇百怪,JSON 的普适性无可替代。
  • 配置文件:用 JSON 或 YAML。不需要考虑兼容性演进,简单就好。
  • 移动端与服务端通信:Protobuf 是更好的选择。省流量,省电量,用户体验更好。

一句话总结:序列化框架的选择,本质是在 性能、兼容性、可调试性 三者之间做权衡。没有银弹,只有最适合你场景的方案。