高 进阶
Protobuf序列化原理#
一句话答案#
Protobuf 使用 Tag-Length-Value (TLV) 编码 + Varint 压缩整数 + ZigZag 编码有符号数,序列化后体积仅 JSON 的 1/3~1/5,解析速度快 5-10 倍,且通过字段编号实现前后兼容。
核心要点
编码原理#
每个字段编码为: Tag + [Length] + Value
Tag = (field_number << 3) | wire_type
Wire Type:
0 = Varint (int32, int64, bool, enum)
1 = 64-bit (fixed64, double)
2 = Length-delimited (string, bytes, 嵌套 message, repeated)
5 = 32-bit (fixed32, float)plaintextVarint 编码(核心压缩手段)#
数字 300 的编码过程:
300 = 0000 0010 0010 1100 (二进制)
拆成 7-bit 组(低位在前):
组1: 010 1100 → 加 MSB=1 → 1010 1100 (0xAC)
组2: 000 0010 → 加 MSB=0 → 0000 0010 (0x02)
编码结果: 0xAC 0x02(仅 2 字节,int32 定长需要 4 字节)plaintext效果: 小整数(< 128)只需 1 字节,大多数业务字段受益。
ZigZag 编码(处理负数)#
// 负数的补码表示很长(-1 = 0xFFFFFFFF),Varint 会变大
// ZigZag 将有符号映射为无符号:
sint32 → (n << 1) ^ (n >> 31)
映射:0→0, -1→1, 1→2, -2→3, 2→4 ...
效果:绝对值小的负数编码也很短plaintext为什么比 JSON 快/小?#
| 维度 | JSON | Protobuf |
|---|---|---|
| 格式 | 文本,含字段名 | 二进制,只有字段编号 |
| 数字 “123” | 3 字节 ASCII | 1 字节 Varint |
| 字段标识 | "name": 重复传输 | Tag 仅 1-2 字节 |
| 解析 | 逐字符扫描 + 字符串比较 | 直接按 offset 读取 |
| 体积 | 1x | 0.2-0.3x |
| 解析速度 | 1x | 5-10x |
前后兼容性#
// v1
message User {
int32 id = 1;
string name = 2;
}
// v2 新增字段
message User {
int32 id = 1;
string name = 2;
string email = 3; // 新增:旧客户端解析时直接跳过未知字段
}
// v3 删除字段
message User {
int32 id = 1;
// name 被删除:不能复用编号 2
reserved 2;
reserved "name";
string email = 3;
}protobuf兼容规则:
- 新增字段:旧代码跳过未知 field_number
- 删除字段:reserved 防止编号复用
- 不能改变已有字段的编号和类型
与其他序列化对比#
| 方案 | 体积 | 速度 | 跨语言 | 可读性 | 兼容性 |
|---|---|---|---|---|---|
| JSON | 大 | 慢 | 好 | 高 | 弱 |
| Protobuf | 最小 | 最快 | 好 | 无 | 强 |
| Hessian | 中 | 快 | Java为主 | 无 | 中 |
| Kryo | 小 | 极快 | Java only | 无 | 弱 |
| Avro | 小 | 快 | 好 | 无 | 强 |
面试回答(2分钟版)
Protobuf 用 TLV 编码,每个字段存的是字段编号而不是字段名,所以体积远小于 JSON。整数用 Varint 编码,每字节用 7 位存数据、1 位做延续标志,小整数只需 1 字节。有符号数用 ZigZag 映射为无符号再 Varint 编码,避免负数补码过长。解析时按 wire_type 直接定位数据边界,不需要像 JSON 那样逐字符扫描和字符串匹配字段名,所以速度快 5-10 倍。兼容性靠字段编号保证:新增字段老客户端跳过未知编号,删除字段用 reserved 禁止编号复用,只要不改已有字段的编号和类型就前后兼容。实际项目中 Protobuf 配合 gRPC 做内部服务通信,对外仍用 JSON(可读性和浏览器兼容)。
追问与易错
追问方向:
- “Protobuf 怎么实现前后兼容?”→ 字段编号不变 + unknown field 跳过 + reserved 防复用
- “Varint 编码大数字反而更大?”→ 是的,>2^28 时 Varint 比 fixed32 大,所以 proto 提供 fixed32/fixed64 类型
- “为什么不用 Protobuf 做存储?”→ 不可读、不支持 range query、schema 演化需要额外管理
- “Protobuf vs Avro?”→ Protobuf 需要 proto 文件生成代码;Avro 可以动态解析(schema 附在数据中),Hadoop 生态常用 Avro
易错点:
- ❌ “Protobuf 字段顺序影响编码”——只有字段编号决定编码,顺序无关
- ❌ “删除字段可以复用编号”——必须 reserved,否则旧数据解析会出错
- ❌ “Protobuf 一定比 JSON 小”——字段全是长字符串且编号很大时差距缩小