大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
消息格式
这一课你将能做到
- 用 JSON 定义
OrderCreated事件信封,经KafkaTemplate发送并用ObjectMapper反序列化消费 - 对照表格说清「新增可选字段 / 删字段 / 改类型 / 改字段名」对兼容性的影响,并给出安全的演进顺序
- 说清实验阶段为何优先 JSON,以及 Avro + Schema Registry 解决什么问题(概念级)
为什么要学
Kafka Broker 只存字节:不帮你校验字段、不帮你做版本迁移。团队如果各写各的 JSON,消费方迟早翻车在「缺字段 / 类型变了 / 同名异义」上——这类事故在日志里只是一行 JsonProcessingException,排查半天才发现是上游偷偷改了字段类型。
消息格式就是 Topic 的公共 API:生产者与消费者的契约。本站 REST 用 Jackson + JSON,事件流先沿用同一套最顺手;等跨语言、多团队共享 Topic、需要自动兼容检查时,再上 Avro + Schema Registry。
人话:Topic 是管道,契约在消息格式。管道不会替你检查内容,契约坏了下游全坏。
先建立图像
Producer Broker Consumer
POJO/Map ──序列化──► bytes ──► 分区日志 ──► bytes ──反序列化──► POJO
│ │
└── 契约:字段名、类型、兼容规则 ──────┘
Avro + Registry(概念图):
Producer 注册 schema → 消息头带 schema id → [magic][id][avro bytes]
Consumer 按 id 向 Registry 取 schema → 反序列化
→ 兼容性在注册时强制检查
| 方案 | 优点 | 代价 | 何时用 |
|---|---|---|---|
| JSON 字符串 | 可读、易调试、与 REST 同栈 | 无强 schema、体积大 | 实验 / 小团队 |
| JSON Schema | 可校验 | 治理靠自觉 | 中等规模 |
| Avro + Registry | 紧凑、多语言、强制兼容检查 | 要运维 Registry | 多团队事件总线 |
跟着做
步骤 1:定一条事件契约(纸上)
写下最小字段,规则:event(类型)+ eventId(幂等键)+ 业务主键 + 发生时间:
{
"event": "OrderCreated",
"eventId": "01JBEXAMPLEUUID",
"orderId": "1001",
"userId": "u42",
"amount": 9900,
"occurredAt": "2026-10-06T00:00:00Z"
}
约定写进笔记:payload 带够用的只读快照(金额、用户),避免消费方事事回查生产方数据库;写路径冲突仍以 MySQL 事务为准。
步骤 2:Java record + 发送
public record OrderCreatedEvent(
String event,
String eventId,
String orderId,
String userId,
long amount,
Instant occurredAt
) {}
// 发送侧:对象 → JSON 字符串(实验序列化策略)
OrderCreatedEvent evt = new OrderCreatedEvent(
"OrderCreated",
UUID.randomUUID().toString(),
"1001", "u42", 9900L, Instant.now());
String json = objectMapper.writeValueAsString(evt);
// key=orderId → 同订单分区内有序(生产者课的纪律)
kafkaTemplate.send("coderoadmap.order-events", evt.orderId(), json);
用 console consumer 核对:Topic 里是可读 JSON,key 是订单号。
步骤 3:消费反序列化
@KafkaListener(topics = "coderoadmap.order-events", groupId = "coderoadmap-notify")
public void onMessage(String payload) throws Exception {
OrderCreatedEvent evt = objectMapper.readValue(payload, OrderCreatedEvent.class);
if (!"OrderCreated".equals(evt.event())) {
return; // 或按类型分发到不同 handler
}
notifyService.handle(evt); // 内部按 eventId 幂等
}
应看到:字段一一映射。故意做一次实验:生产者先发一条缺 amount 的消息,观察 Jackson 行为——record 构造器缺参会抛异常。这说明反序列化失败要进死信/告警(上一课的 DefaultErrorHandler),不能静默吞掉后照常 ack。
步骤 4:演一出「兼容」小实验
- 生产者新增可选字段
"note":"hello",消费者的 record 暂不声明。 - 给 ObjectMapper 配
FAIL_ON_UNKNOWN_PROPERTIES=false(Boot 默认已关),老消费者照常工作。 - 笔记写结论:新增可选字段 ≈ 安全;删字段或改类型要先升级消费者再升级生产者。
# Boot 默认行为(可显式声明):
spring.jackson.deserialization.fail-on-unknown-properties: false
步骤 5:设计 TrackUpdated 事件
为本站课程域写一版:
{
"event": "TrackUpdated",
"eventId": "01JC...",
"trackId": "kafka-essentials",
"slug": "kafka-essentials",
"title": "Kafka 精讲",
"version": 2,
"occurredAt": "2026-10-06T01:00:00Z"
}
回答两个问题:哪个字段做消费幂等键(eventId,全局唯一不复用)?version 服务什么(实体级乐观并发/投影排序)?
步骤 6:扫一眼 Avro 流程(不必部署)
在笔记画:
Producer → Registry 注册 schema(id=42) → Broker: [magic][42][avro-bytes]
Consumer → 用 42 拉 schema → 反序列化
Registry 拒绝不兼容 schema 注册(BACKWARD 兼容模式)
本课不强制起 Registry 服务;理解「把契约检查从事故现场挪到注册时」即可。
讲清楚要点
- 因为Broker 不解释业务字段,所以格式与兼容规则是团队契约,要写进文档并过评审——改字段 ≈ 改接口。
- 因为JSON 人类可读,所以实验与排错成本低;
kafka-console-consumer直接能看,事故现场肉眼可查。 - 因为消费方可能滞后部署,所以演进铁律:只加不改不删——新增可选字段安全;删字段、改类型、改字段名都是破坏性变更,要走双写/版本过渡。
- 因为只传 id 会强迫每个消费者回查生产方库,所以事件带只读快照字段;但快照是「发生时」的值,权威修正仍回 MySQL。
- 因为
eventId稳定唯一,所以它同时服务幂等(去重表键)与追踪(日志 grep 一个 id 贯穿全链路)。 - 因为类型是契约的一部分,所以金额用
long(分)不用浮点、时间用 ISO-8601/epoch millis 不用本地化字符串——这些坑比「加字段」更常见。
你可能会懵的地方
-
「Kafka 能帮我校验 JSON Schema 吗?」 默认不能,Broker 只看字节。校验发生在生产者(序列化前)、消费者(反序列化时)或 Schema Registry(Avro/Protobuf 注册时)。
-
「新增字段老消费者一定挂?」 不是。消费方配置忽略未知属性(Boot 默认)时,新增可选字段通常安全;危险的是删字段、改类型、改语义(同名字段含义变了)。
-
「Avro 是不是必须学?」 单团队、Java 栈、JSON 够用;多团队共享 Topic、跨语言、要自动兼容检查时才上。面试答出「Registry 解决的是契约治理问题」即可加分。
-
「和 Redis 里存 JSON 有什么不同?」 存储的都是字节,但职责不同:Redis 的 JSON 是缓存/状态(可随时重算覆盖),Kafka 的 JSON 是历史事件流(不可变,用于重放与审计)。缓存丢了重算,事件丢了要靠对账补。
-
「事件里要不要放全量订单数据?」 不放全量,放消费方够用的快照。全量导致事件与表结构强耦合,表加列事件就得跟着改;够用原则 + 需要时消费方回查/订阅后续事件。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
各服务随便拼字段,无 event/eventId | 消费方无法分发、无法幂等 | 统一信封:类型 + 幂等键 + 时间 |
消息只传 orderId,人人查库拼数据 | 与生产方库强耦合、放大查询压力 | 带只读快照;库作补偿与修正 |
| 直接改字段类型上线(amount: long→string) | 老消费者反序列化全挂 | 新增字段过渡,双写期后下线旧字段 |
| 反序列化失败吞异常还继续 ack | 毒丸消息静默丢失 | 不 ack 进重试,重试耗尽投死信并告警 |
| 金额用 double 存进事件 | 精度丢失,对账对不上 | long 存分 / BigDecimal+字符串 |
卡点急救
- 消费报
UnrecognizedPropertyException:消费方开启了未知字段校验。统一 Boot 默认(忽略未知字段),并把它写进团队规范,而不是每个服务各配各的。 - 上游加了字段下游没感知就上线:契约评审流程 + 消费端用「宽松反序列化 + 必填字段断言」双保险;必填字段缺失要显式报错进死信,而不是拿 null 继续算。
- 同一条消息两个消费方解析结果不一致:多半是两端 Jackson 版本/配置漂移。把 record 类抽到共享
event-contract模块(或代码生成),契约只有一个来源。
面试常问
-
为什么消息要有统一格式/信封? 三个理由:①分发——
event字段让一个 Topic 可以承载多类事件并按类型路由;②幂等——eventId是去重表唯一键,重复投递可安全跳过;③排障——occurredAt+eventId能把生产、Broker、消费三段日志串成一条时间线。没有信封的消息只能当不透明字符串处理。 -
Schema 演进如何做到不破坏消费者? 原则「只加不改不删」:新增字段给默认值且可空;废弃字段先标记 deprecated 停止写入、观察期后再删;类型不变、语义不变。用 Avro + Registry 时选 BACKWARD 兼容模式,注册即校验;纯 JSON 时靠评审 + 宽松反序列化 + 必填断言兜底。
-
JSON 和 Avro 怎么选? JSON:可读、调试方便、生态零成本,适合小团队/实验;Avro:二进制紧凑(省存储与带宽)、强 schema、Registry 强制兼容检查、多语言代码生成,适合大规模事件总线。中间态也可以 JSON + 团队规范,等痛了再迁。
自测三问
- 为
TrackUpdated列出 JSON 字段,并指出哪个做消费幂等键、为什么不是version。 - 新增可空
note字段时老消费者为何仍工作?直接把amount从 long 改成 string 会发生什么? - Redis 存 JSON 与 Kafka 消息 JSON:各举一个本站更合适的场景。
小结一句话
先 JSON 统一信封(event + eventId + 快照 + 时间)并守住「只加不改不删」;规模与多语言上来再上 Avro + Schema Registry。
延伸
- Kafka 官方文档 — 序列化配置
- 下一课「投递语义」:at-least-once / exactly-once 到底指什么,幂等与 Outbox 如何落地。