先说结论

序列化是把对象转换为可传输或保存的数据格式,反序列化执行逆过程。业务系统通常根据可读性、体积、性能、跨语言和兼容性选择协议,不应默认使用 JDK 原生序列化。

JDK 序列化依赖 SerializableserialVersionUID 用于版本校验,transient 字段默认不参与序列化。它与 Java 类型绑定较深,且历史上存在危险的反序列化调用链。

关键机制

JSON 可读且生态成熟,但类型和体积控制较弱;Protobuf 使用 schema 和字段编号,体积小、跨语言,新增可选字段通常容易兼容,但修改字段语义或复用已删除编号会破坏协议。

常见方案对比

方案 可读性 跨语言 体积与速度 演进方式 常见场景
JDK 原生 体积较大 依赖 Java 类结构 受控的短期内部数据,通常不推荐做公共协议
JSON 体积较大、解析成本中等 依靠字段约定 HTTP API、配置、调试友好链路
Protobuf 紧凑且高效 schema + 字段编号 RPC、消息、跨语言高吞吐链路
Avro 紧凑 schema 解析与注册 数据平台、事件流、批处理

选型不能只跑一次序列化 benchmark。链路中通常还包含压缩、网络、对象分配、Schema Registry 和灰度兼容,端到端指标才有意义。

协议演进规则

一个生产者和多个不同版本消费者会长期共存,协议必须允许滚动升级。常见原则包括:

  • 新字段提供默认语义,旧消费者可忽略未知字段。
  • 不改变已有字段编号、类型和业务含义。
  • 删除的 Protobuf 字段编号标记为 reserved,永不复用。
  • 枚举新增值时,消费者必须能处理未知值,而不是进入非法状态。
  • 结构性大改使用新事件类型或主版本,避免一个布尔字段改变整条消息解释。

例如把金额从“元的浮点数”改成“分的整数”即使字段类型兼容,语义也不兼容。正确做法是新增 amount_cent,双写双读完成迁移,再逐步废弃旧字段。

反序列化安全

反序列化不是单纯的数据解析,它可能实例化类型、调用钩子或消耗巨大资源。外部入口应限制消息大小、嵌套深度、集合长度和可实例化类型,依赖库及时升级,并避免对不可信字节使用 JDK 原生对象反序列化。

JSON 也并非天然安全。开启任意多态类型解析可能让攻击者指定危险类;超深 JSON、超大数字和压缩炸弹也会造成 CPU 或内存耗尽。

事件设计案例

订单事件建议传递稳定事实,而不是直接序列化 ORM 实体:

{
  "event_id": "evt_01",
  "event_type": "order.paid.v1",
  "occurred_at": "2026-07-22T10:00:00Z",
  "order_id": "O1001",
  "amount_cent": 9900,
  "currency": "CNY"
}

事件 ID 用于幂等,类型与版本确定解释方式,时间采用明确时区,金额避免浮点误差。消费者只依赖事件契约,不依赖生产者内部 Java 类名。

实际用时要注意什么

  • 网络入口只接收白名单类型和受控协议。
  • 消息与缓存内容必须携带版本,支持灰度期间新旧消费者共存。
  • 不把数据库实体直接当长期外部协议,避免内部字段变化外溢。

容易踩坑的地方

序列化成功不代表协议可长期演进;性能也不能只看编码耗时,还要看数据体积、网络、GC 和 schema 管理成本。

常见问题

追问:serialVersionUID 不一致会怎样?

JDK 反序列化会进行版本检查,不一致通常抛出 InvalidClassException。显式声明只能控制版本标识,不能自动解决字段语义不兼容。

追问:transient 字段一定不会被序列化吗?

它不会参与默认 JDK 序列化,但类可通过自定义 writeObject/readObject 处理数据;其他 JSON 或二进制框架也有自己的忽略规则,不能把 transient 当通用协议注解。

追问:为什么不直接把数据库实体发送到 Kafka?

实体包含持久化细节、懒加载关系和频繁变化字段,会让消费者与生产者内部模型强耦合。独立事件 DTO 更容易做最小披露、版本演进和长期治理。

追问:压缩应该放在序列化前还是后?

先序列化得到字节,再对批次或消息压缩。批量压缩通常比单条压缩效果好,但要限制解压后大小并评估 CPU 与延迟。