面试知识库
高 进阶

Protobuf序列化原理#

一句话答案#

Protobuf 使用 Tag-Length-Value (TLV) 编码 + Varint 压缩整数 + ZigZag 编码有符号数,序列化后体积通常只有 JSON 的几分之一、解析也快得多(常见说法小 3-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, packed repeated)
  (3/4 = group 开始/结束,已废弃)
  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,int32/int64 的负数按 64 位符号扩展后做 Varint,固定占 10 字节
// 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 字节(编号 1-15 占 1 字节,16-2047 占 2 字节)
解析逐字符扫描 + 字符串比较顺序读 Tag,按 wire_type/Length 直接切出值或跳过,无需字符串匹配
体积1x常见 0.2-0.3x(视数据而定)
解析速度1x常见 5-10x(视语言/实现而定)

前后兼容性#

兼容规则:

  • 新增字段:旧代码跳过未知 field_number
  • 删除字段:reserved 防止编号复用
  • 不能改变已有字段的编号;类型只能在线格式兼容的组内改(如 int32/uint32/int64/bool 之间),否则会解析错

与其他序列化对比#

方案体积速度跨语言可读性兼容性
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 可以不生成代码、按 writer schema 动态解析(schema 放在容器文件头或由 Schema Registry 提供),Hadoop/Kafka 生态常用 Avro
  • “proto3 怎么区分「没设置」和「设成默认值 0」?”→ proto3 普通标量字段是隐式存在性(implicit presence),默认值不上线、也无法判断是否设置;3.15 起可给字段加 optional 恢复 has 方法(explicit presence),或用 wrapper 类型/oneof。新的 Editions(Edition 2023/2024)用 features.field_presence 统一 proto2/proto3 语义,默认就是 EXPLICIT

易错点:

  • ❌ “Protobuf 字段顺序影响编码”——只有字段编号决定编码,顺序无关
  • ❌ “删除字段可以复用编号”——必须 reserved,否则旧数据解析会出错
  • ❌ “Protobuf 一定比 JSON 小”——字段全是长字符串且编号很大时差距缩小