面试知识库
进阶

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)
plaintext

Varint 编码(核心压缩手段)#

数字 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 快/小?#

维度JSONProtobuf
格式文本,含字段名二进制,只有字段编号
数字 “123”3 字节 ASCII1 字节 Varint
字段标识"name": 重复传输Tag 仅 1-2 字节
解析逐字符扫描 + 字符串比较直接按 offset 读取
体积1x0.2-0.3x
解析速度1x5-10x

前后兼容性#

兼容规则:

  • 新增字段:旧代码跳过未知 field_number
  • 删除字段:reserved 防止编号复用
  • 不能改变已有字段的编号和类型

与其他序列化对比#

方案体积速度跨语言可读性兼容性
JSON
Protobuf最小最快
HessianJava为主
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 小”——字段全是长字符串且编号很大时差距缩小