SERIALIZATION · DATA INTERCHANGE
序列化到底是什么?
JSON / Protobuf / Avro / Thrift 差在哪
程序里的对象住在内存中,是一堆带指针的结构;要把它发到网络对面、写进磁盘、塞进缓存,就得先变成一串字节。这个「对象 → 字节流」的翻译过程就是序列化,反过来的过程叫反序列化。本文用图 + 可交互的字节透视器,把四种主流格式的差别一次讲透。
1什么是序列化
内存里的对象是「靠地址和指针拼起来的」:0x7f3a2c00 这种地址在别的进程、别的机器上毫无意义。序列化就是把对象拍平成一串自包含、可顺序读取的字节,让数据能「旅行」。
图 1 · 序列化:把「带指针的内存结构」翻译成「一维自包含的字节流」
📦
拍平结构
嵌套对象、数组、map 都要展开成线性的字节序列,读的时候再重建。
🧭
写清边界
字符串多长?字段从哪开始到哪结束?格式必须自带「说明书」,否则读不回来。
🔁
能还原
反序列化必须能在另一台机器、另一种语言里,重建出语义等价的对象。
💡 一个直觉类比
序列化就像「搬家打包」:家具(对象)没法直接塞进卡车,必须拆成零件、贴上编号、装进箱子(字节流);到了新家再按图纸装回去(反序列化)。不同的打包公司(JSON / Protobuf / Avro / Thrift)打包方式、箱子大小、说明书写法都不一样——这就是它们差别的来源。
2序列化用在哪里
只要有「数据要离开当前进程」的时刻,就有序列化。四个最典型的位置:
图 2 · 四大主战场:跨进程通信、缓存、持久化、消息传递
⚠️ 为什么格式选择很值钱
一个日均 10 亿次的 RPC 接口,如果序列化能省 30% 体积,就直接省下 30% 的带宽和相当一部分反序列化 CPU;反过来,一个高频接口选了笨重的格式,浪费的是真金白银的服务器。差别不是学术问题,是成本问题。
3差别从何而来:三个根本设计抉择
JSON、Protobuf、Avro、Thrift 的所有差别,几乎都能归结到三个「选择题」上。先建立这个坐标系,后面每一种技术的行为都会变得可以推理。
📝
抉择一:文本 or 二进制?
用「人类可读的字符」编码,还是「紧凑的原生字节」编码?
文本(JSON):人能直接看,调试爽,但体积大、解析要逐字符扫描。
二进制(其余三家):体积小、解析快,但肉眼不可读,必须靠工具。
📜
抉择二:Schema 放哪里?
数据的「字段名 + 类型」说明书,是随数据一起传,还是提前用 IDL 定义好、双方各自编译?
不随数据传(Protobuf/Thrift):体积省,但必须先交换 schema、做代码生成。
随数据存(Avro 文件):自描述,天然适合存档。
🔬
抉择三:只管格式 or 连 RPC 一起管?
只定义「字节长什么样」,还是顺便把跨语言的 RPC 框架、传输协议都打包给你?
只有格式(JSON、Avro、Protobuf 本体)。
全家桶(Thrift,Protobuf+gRPC 也算半个)。
图 3 · 四种技术的定位地图:同做序列化,取舍完全不同
4JSON:为人而生的文本格式
JSON(JavaScript Object Notation)用「字符」表达一切:数字 150 在网络上是 3 个字符(0x31 0x35 0x30),不是 1 个字节。字段名 "name" 原样出现在每一条消息里。
JSON
{
"id":
150,
"name":
"Alice",
"email":
"alice@example.com",
"active":
true
}
图 4 · JSON 编码:可读性带来的「语法税」
JSON 的优势与代价
✅
优势
· 人类直接可读,调试、日志、排查问题极方便
· 无需预定义 Schema,浏览器原生支持,跨语言零成本
· 生态无敌:REST API 事实标准,一切工具都认识它
❌
代价
· 体积大:字段名重复 + 文本编码数字,通常比 Protobuf 大 3~10 倍
· 解析慢:文本解析 + 大量小字符串分配
· 类型弱:没有 int64/区分不了 int 和 float、没有日期类型、二进制数据必须 Base64 再膨胀 33%
5Protobuf:用「编号 + 类型」压缩一切
Protobuf 的两个核心点子:① 先用 IDL 定义结构,编译生成各语言的代码;② 数据里不再写字段名,只写「字段编号 + 类型 + 值」。字段 name 变成了 1 个字节的 0x12。
user.proto
syntax =
"proto3";
message User {
int32 id =
1;
// ← 字段编号是灵魂
string name =
2;
string email =
3;
bool active =
4;
}
// protoc --go_out=. user.proto → 生成 Go 代码
字段编号 + 类型合成了一个「tag 字节」:低 3 位是 wire type(值的编码方式),高 5 位是字段编号。
例如字段 2(name,string)→ tag = 2 << 3 | 2 = 0x12,只需 1 个字节就替代了 JSON 里的 "name":(8 个字节)。
整数用 Varint 编码:小数字占更少字节。150 → 0x96 0x01(2 字节);带符号负数先用 ZigZag 映射成正数再 Varint。
图 5 · Protobuf wire format:tag(字段编号+类型) + 值,紧凑到极致
编号带来的超能力:前后兼容
🔁 新增字段 = 前向兼容
旧程序读到不认识的字段编号时,根据 wire type 跳过(或暂存到 unknown fields),不会报错。服务端先升级加字段,老客户端照常工作。
🔁 删除字段 = 后向兼容
只要不回收、不重用已删除字段的编号(预留 reserved),新老版本就永远不冲突。所以 proto 文件里常见 reserved 5, 6;。
⚠️ Protobuf 的铁律
字段编号一旦发布就不能再改含义。改编号 ≈ 换字段名还不通知对方,线上会出现「张冠李戴」的静默数据错乱。另外它是二进制格式:排查问题要靠 protoc --decode 这类工具,肉眼无法直接读。
6Avro:Schema 会「随行」和「演化」
Avro 出身 Hadoop 生态(作者 Doug Cutting 也是 Hadoop 之父),核心创新是「写数据时用的是哪个版本 Schema,读数据时用的是哪个版本 Schema,两者可以不同」——靠一个叫 Schema Resolution(模式解析) 的算法按字段名对齐。
user.avsc(注意:Schema 本身就是 JSON)
{
"type":
"record",
"name":
"User",
"fields": [
{
"name":
"id",
"type":
"int" },
{
"name":
"name",
"type":
"string" }
]
}
图 6 · Avro 双 Schema 机制:新旧版本各读各的,永远能对话
📦 Avro 容器文件
存文件时 Schema 写在文件头部,数据块之间有 sync 标记——这让 Hadoop/Spark 可以按块并行切分处理一个大文件,且多年后读回时依然自描述。数据湖场景(含 Parquet 的思想渊源)大量受益于此。
⚡ 极致紧凑
因为双方都握着 Schema,编码时连 Protobuf 那个 tag 字节都不需要——只按顺序写值。同样数据通常是四者中最小的。Kafka 社区(Confluent)用它 + Schema Registry 做消息格式是经典组合。
7Thrift:不只是格式,是整套 RPC
Thrift 由 Facebook 开源(2007),动机就是「跨语言的 RPC 太难写」。所以它的定位和另外三个不同:序列化格式只是它三层架构中最底下的一层,上面还长着传输层和 RPC 服务层。
user.thrift(注意:不只定义数据,还定义方法)
struct User {
1:
i32 id,
2:
string name,
3:
bool active
}
service UserService {
User getUser(
1:
i32 id),
// ← RPC 方法签名
list<
User>
listUsers(
1:
i32 limit),
void addUser(
1:
User u),
}
// thrift --gen go user.thrift → 一次生成:数据结构 + 序列化 + 客户端桩 + 服务端骨架
图 7 · Thrift 的「全家桶」:IDL 一次生成 Client/Server 两端所有层的代码
🆚 Thrift vs Protobuf+gRPC:今天怎么选
两者解决同一个问题(跨语言 RPC + 二进制序列化)。历史上 Thrift 出现更早、自带服务端线程模型;而如今 gRPC 生态(HTTP/2、流式、负载均衡、鉴权拦截器)更繁荣,新项目多选 Protobuf + gRPC。Thrift 的 TCompactProtocol 编码思路(field id + zigzag varint)与 Protobuf wire format 非常接近,学会一个就懂另一个。
8交互演示:同一份数据的编码透视
以 { "id": 150, "name": "Alice", "active": true } 为例(不含 email,方便逐字节看清)。切换标签,看四种格式分别把它编码成什么样子、各占多少字节。也可以改改 id 和 name 试试。
* 字节数为按各格式规范推算的示意值(JSON 含换行缩进前的紧凑形式;Thrift TCompactProtocol 的消息内字段编码)。真实实现可能有 1~2 字节出入,用于感受量级差异。
9对比总表
一张表收拢全部差别——可以先扫「定位」一行,再按需细看。
| 维度 |
📝 JSON |
⚡ Protobuf |
🍃 Avro |
🔧 Thrift |
| 出身 |
源于 JS 语言,2001 由 Douglas Crockford 推广 |
Google,2008 开源 |
Apache / Hadoop 体系,Doug Cutting |
Facebook,2007 开源 |
| 编码形态 |
文本 |
二进制 |
二进制 |
二进制(TJSON 可选) |
| 可读性 |
肉眼直接读 |
需工具解码 |
需工具 + Schema |
TJSON 可读,二进制需工具 |
| 体积(同数据) |
大(基准 1x) |
约 1/3~1/10 |
最小(无 tag 字节) |
与 Protobuf 相当 |
| Schema 定义 |
无(JSON Schema 非核心) |
.proto IDL + 编译器 |
.avsc(本身是 JSON) |
.thrift IDL + 编译器 |
| 字段标识方式 |
字段名字符串,每条都传 |
字段编号 + wire type(tag 字节) |
不编码标识,按 Schema 顺序,按字段名解析 |
字段编号 + 类型(TCompact 与 pb 类似) |
| Schema 演化 |
随意(反正没人管) |
靠编号规则:加/删字段安全,禁改号 |
最灵活:按名解析 + alias + 默认值 |
与 Protobuf 类似的编号规则 |
| 代码生成 |
通常不需要 |
必须(protoc) |
可选(可纯运行时解析,动态语言友好) |
必须(thrift 编译器) |
| RPC 能力 |
无(配 REST/HTTP 手写) |
本体无,配 gRPC 成全家桶 |
有内置 RPC(但少用,多用于消息/存储) |
自带完整 Client/Server RPC 栈 |
| 典型场景 |
REST API、配置、日志、浏览器交互 |
微服务 gRPC、内部高吞吐接口 |
Kafka 消息、数据湖、Hadoop 存储 |
多语言 RPC、遗留大系统内部通信 |
| 一句话定位 |
为人服务:可读优先 |
为机器服务:小而快 |
为数据服务:演化优先 |
为工程服务:一站优先 |
10场景选型
点选你的场景,看推荐结论(也可以多个场景都看看,选型往往是几个约束的加权)。
🌐 对外 REST API
给第三方/前端调用的公开接口
⚙️ 内部微服务高频 RPC
QPS 高、服务间内网调用
📮 Kafka 消息流 / 事件管道
生产者消费者可能不同步升级
💾 数据湖 / 长期归档存储
几年后还要被新团队新引擎读
🏢 多语言遗留系统互联
C++ / Java / Python / Go 混合老系统
图 8 · 一棵决策树收尾:先问可读性,再问场景,最后看生态
11实战坑点
选型之外,真正让线上出事的往往是这些细节:
🔥 Protobuf:改字段编号 = 数据错乱
编号是唯一标识,删掉字段 5 又新建字段 5,老数据会被解析进新字段。务必用 reserved 占住已删编号。
🔥 JSON:大整数精度丢失
JS 的 Number 只有 53 位安全整数,订单号/雪花 ID(64 位)经 JSON 传到浏览器会丢精度。约定「超长整数传字符串」。
⚠️ Avro:Schema 没管好 = 解析失败
双 Schema 机制的前提是「能拿到写入时的 Schema」。Kafka 场景必须配 Schema Registry 并制定兼容性策略(BACKWARD 等),否则消费者直接崩。
⚠️ Thrift:两端协议/传输配错 = 玄学超时
服务端 TCompactProtocol + 客户端 TBinaryProtocol,连得上但解不出数据。客户端构造时必须与服务端 Protocol/Transport 严格一致。
💡 反序列化是攻击面
历史上 Java 原生反序列化、Python pickle 都出过 RCE 漏洞。永远不要反序列化「不可信来源」的数据;跨信任边界优先选无执行能力的格式(本文四种都远好于 pickle/Java 原生)。
💡 别忘了压缩可以叠加
文本格式的劣势可部分被 gzip/snappy 找回(HTTP JSON + gzip 很常见)。但压缩消耗 CPU 且对小消息效果有限,二进制格式 + 压缩仍然更快更小。
12总结
🧠
序列化的本质
把带指针的内存结构翻译成自包含的一维字节流。一切格式都在回答:字段怎么标识、类型怎么表达、边界怎么划分、Schema 放哪里。
⚖️
差别的根源是取舍
可读 vs 体积、自描述 vs 预编译、纯格式 vs 全家桶。没有最好的格式,只有匹配场景的格式。
JSON
为人而生,可读优先,REST 的事实标准。
Protobuf
为机器而生,编号 + varint,小而快,配 gRPC。
Avro
为数据而生,双 Schema 按名解析,演化自由,消息/数据湖标配。
Thrift
为工程而生,IDL 到 RPC 的一站式全家桶。
🎯 一句话带走
对外用 JSON 让人看懂,内部 RPC 用 Protobuf 让机器省电,消息管道和存储用 Avro 让数据活得久,Thrift 留给需要整套框架的地方——它们不是竞争关系,而是分别站上了「人、机器、数据、工程」四个不同的出发点。