← 返回题目列表

Protocol Buffers 是什么?它相比 JSON 有什么优势和代价?

高频 中等 第 8 / 27 题 更新于 2026/07/31
Protobuf序列化gRPCJSON

简化版

Protocol Buffers 是一种基于 schema 的二进制序列化协议,先用 .proto 定义字段编号和类型,再生成多语言代码。它通常比 JSON 更小、更快、类型更明确,适合内部 RPC 和高吞吐服务通信;代价是可读性差、调试需要工具、字段演进必须遵守兼容规则。

详细版

Protobuf 的核心不是“二进制所以快”,而是“字段有编号、类型固定、编码紧凑、代码可生成”。JSON 每个对象都要携带字段名,例如 {"user_id":123},而 Protobuf 在线上只传字段编号和编码后的值。对于大量重复结构,节省很明显。

维度JSONProtobuf
数据格式文本二进制
schema可选,常靠约定必须定义 .proto
可读性人能直接读需要工具解码
体积字段名重复,较大字段编号,较小
类型运行时解释编译期生成类型

面试重点通常包括:字段编号不能随便改;删除字段要 reserved;新增字段一般向后兼容;默认值会影响“字段是否存在”的判断;Protobuf 不适合所有公网接口,因为调试和生态门槛比 JSON 高。

完整版教学

一、Protobuf 解决的是序列化效率和契约问题

服务之间通信时,内存对象不能直接在网络上传,需要变成字节流,这就是序列化。JSON 的优势是通用、可读、调试方便,但它没有强制 schema,并且字段名会在每条消息里重复出现。Protobuf 则要求先定义消息结构,再生成代码,发送时只传紧凑的二进制数据。

例如 100 万条用户消息,每条 JSON 都带 "user_id""name""active" 这些字段名,字段名本身就会产生大量重复开销。Protobuf 用字段编号表示字段,user_id 可以是编号 1,name 是编号 2,线上数据里不再重复传字段名。

message User {
  int64 user_id = 1;
  string name = 2;
  bool active = 3;
}

二、字段编号为什么比字段名更重要

Protobuf 的兼容性围绕字段编号展开,而不是字段名。生成代码里你看到的是 user_id,但网络上传输时识别字段靠的是 1。如果把 user_id = 1 改成 user_id = 5,旧服务发来的字段 1,新服务就不再当成 user_id,兼容性会被破坏。

字段编号和 wire type 一起组成 key:

key = (field_number << 3) | wire_type

例如字段编号 1、wire type 0 的 key 是 (1 << 3) | 0 = 8。这就是为什么字段编号一旦发布就要稳定,字段名反而可以在代码层谨慎重命名。

记 Protobuf 兼容性时抓住一个点:线上认的是字段编号,不是字段名。

三、为什么 Protobuf 体积通常更小

Protobuf 对整数使用 varint 编码,小整数占用更少字节。例如数字 150 用普通 32 位整数固定要 4 字节,而 varint 只要 2 字节。再加上字段名不传输,所以对象越多、字段名越长、数值越小,Protobuf 优势越明显。

150 的 varint 编码:
150 = 0b10010110
拆成 7 位组后编码为两个字节: 0x96 0x01

但不是所有数据都会压缩到非常小。字符串、图片、已经压缩过的数据本身占主体时,Protobuf 只能省掉结构开销。面试里不要绝对化地说“Protobuf 一定比 JSON 小十倍”,要看字段类型和数据分布。

四、为什么 Protobuf 速度通常更快

JSON 解析需要读取文本、识别引号、冒号、逗号、转义字符,还要把字符串字段名映射到对象字段。Protobuf 解析的是结构化二进制,按字段编号和 wire type 读取,生成代码后可以直接写入强类型字段。

处理路径对比:

JSON: 字节 -> 字符串解析 -> 字段名匹配 -> 类型转换 -> 对象
PB:   字节 -> key/wire type -> 按编号赋值 -> 对象

速度优势在高 QPS 内部服务里明显。假设一个接口每秒 50000 次调用,每次 JSON 解析多消耗 40 微秒,就会多出约 2 秒 CPU 时间每秒,需要更多核数兜住。

五、schema 演进要遵守兼容规则

Protobuf 的强约束带来一个责任:schema 不能随便改。常见安全操作是新增字段;旧服务收到未知字段会忽略,新服务读不到旧数据里的新字段时使用默认值。危险操作是复用字段编号、改变字段类型、删除字段后又把编号给别人。

操作是否安全原因
新增 optional string email = 4通常安全旧版本忽略未知字段
修改 int32 id = 1string id = 1危险wire type 或语义变化
删除字段后 reserved 3安全防止编号被误复用
字段改名但编号不变通常可行网络上传输不依赖字段名

删除字段时最好写:

reserved 3;
reserved "old_field";

六、默认值和“字段是否存在”容易踩坑

在 Protobuf 里,数字默认是 0,布尔默认是 false,字符串默认是空串。问题是:字段没传和字段传了默认值,在某些版本或写法下可能区分不出来。比如年龄字段 age=0,到底是没有填写,还是新生儿年龄就是 0?

解决方式是使用 optional、wrapper 类型,或者业务上额外设计状态字段。面试中可以举例:账户余额为 0 是有效值,不能简单用 balance == 0 判断字段缺失。

message Account {
  optional int64 balance_cent = 1;
}

七、Protobuf 适合内部 RPC,不一定适合所有开放 API

Protobuf 的生态和 gRPC 结合紧密,非常适合微服务内部通信。内部服务更看重性能、契约和多语言生成,调试工具也可以统一配置。但公网开放 API 往往更重视易接入、可读性、浏览器和开发者工具支持,JSON 仍然有优势。

选型时可以这样看:

场景推荐
内部高 QPS RPCProtobuf
移动端弱网且 SDK 可控Protobuf 可考虑
第三方开放平台JSON 更友好
浏览器直接调用JSON 更自然
日志和配置文件JSON/YAML 更易读

所以 Protobuf 的价值不是替代 JSON,而是在明确 schema、高性能、多语言 RPC 的场景里更合适。

八、常见误区与追问

  • 误区:Protobuf 快只是因为二进制。 更关键的是 schema 固定、字段编号紧凑、生成代码解析路径短。
  • 误区:字段名不能改。 网络上传输依赖字段编号,字段名可谨慎改,但生成代码和业务引用要同步处理。
  • 误区:删除字段后编号可以马上复用。 复用编号会让旧数据被新字段误读,应使用 reserved
  • 误区:Protobuf 一定适合公网 API。 公网 API 更看重可读、易调试和低接入门槛,JSON 经常更合适。
  • 追问:新增字段为什么通常兼容? 旧版本解析时会忽略未知字段,新版本读旧消息时使用默认值。
  • 追问:Protobuf 如何表示字段缺失? 可以用 optional、wrapper 类型或业务状态字段,不能总靠默认值判断。
  • 追问:gRPC 和 Protobuf 是什么关系? gRPC 常用 Protobuf 定义服务和消息,但 Protobuf 本身只是序列化和接口描述能力。

九、加强记忆

Protobuf 要记住三件事:传输靠字段编号,不靠字段名;体积小来自字段编号和 varint 等紧凑编码;兼容性靠“不改编号、不乱改类型、删除要 reserved”。它很适合内部 RPC 和高吞吐通信,但调试和开放生态不如 JSON 直接。回答时把效率、契约、兼容演进和适用边界一起讲出来。