17.1 事件契约、投递语义与消费者幂等
事件驱动架构用已发生的事实连接生产者和消费者。它减少时间上的直接耦合,却把契约、重复、顺序、延迟和恢复变成一等设计问题。
事件不是“异步的函数调用”。先区分命令与事件:
- 命令表达期望:
SettleMatch,通常有目标接收者,可能被拒绝; - 事件表达事实:
MatchSettled,由事实所有者发布,可以有零到多个消费者。
把命令命名成过去式事件,会掩盖责任;把事件命名成 UpdateRanking,会让发布者替消费者决定动作。
事件包含足够而稳定的语义
下面使用项目自定义信封,给出完整的示例事件 ID 和 W3C 追踪上下文。它不是 CloudEvents 格式;若采用 CloudEvents 1.0,应使用其必需属性 id、source、specversion、type,并按扩展规则承载业务元数据。
{
"eventId": "b7b6b00b-107e-4b8b-a302-6138f4390281",
"eventType": "MatchSettled",
"schemaVersion": 2,
"aggregateId": "match-8472",
"aggregateVersion": 9,
"occurredAt": "2026-08-03T10:15:30Z",
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"data": {
"winnerId": "player-42",
"loserId": "player-17"
}
}eventId 在生产者重试和 relay 重发时必须保持不变,用于去重;多来源事件还需要明确 ID 的唯一性范围。聚合版本可以检测乱序,在收到完整连续版本流时还能检测缺口。occurredAt 是业务事实发生时间而非 broker 接收时间。敏感信息和随时可变的大对象不应被无边界复制到事件中。
事件可以携带消费者完成常见动作所需的稳定快照,避免所有消费者收到事件后立刻同步回查生产者,重新制造时空耦合。这里的胜负 ID 足够驱动结果通知,却不足以确定积分增量;若积分依赖赛前评分或规则版本,事件还需携带相应事实,或引用能按版本读取的记录。
投递语义是端到端属性
消息基础设施常提供:
- at-most-once:对一次被接受的消息最多交付一次,可能丢失;不保证生产者不会把同一业务动作当成两条消息发送;
- at-least-once:在约定的持久化、保留和持续重试条件下至少交付一次,可能重复;
- 某个受限范围内的 exactly-once 处理能力。
即使 broker 支持事务,数据库、外部 API 和消费者副作用仍可能在其事务范围之外。业务上更实用的目标通常是:至少一次投递 + 幂等业务效果 + 对账修复。
消费者只有在业务结果安全提交后才能确认消息。先确认再处理会在崩溃时丢消息;处理后确认意味着确认失败时可能重复投递。Kafka 的提交 offset 表示下次从哪里继续:若 offset 5 失败、6 成功,直接提交 7 会跳过 5。并行处理时必须追踪连续完成的位置,保留 5 为恢复起点,并允许 6 被幂等重放。
顺序只在需要的范围内保证
全局顺序昂贵且通常没有业务意义。以稳定的 aggregateId 作为分区键,可把同一比赛的事件送到同一分区,同时让不同比赛并行处理。但生产者和 relay 仍须按业务顺序发布,消费者须按键顺序应用;分区只能保留写入顺序,不能把先发出的版本 9 自动排到版本 8 后面。
消费者仍要处理:
- 重复版本:确认事件 ID 和内容相同后幂等忽略,内容不同则报冲突;
- 旧版本晚到:拒绝或记录;
- 版本缺口:暂停该键、补取或进入修复流程;
- 多源事件:不要假设不同 topic 之间有统一顺序。
Schema 演进遵循兼容窗口
生产者和消费者不会同时升级。常见兼容变更包括新增可选字段、给读取方提供默认值、保持旧语义不变,但具体是否兼容取决于序列化格式和校验器。严格拒绝未知字段的 JSON 消费者,照样会被一个“只是新增”的字段卡住。
重命名、改变类型、删除字段或复用旧字段表达新含义属于破坏性变更,应采用新版本并经历:
消费者先支持新旧版本
→ 生产者发布新版本
→ 检查线上消费、保留消息和重放任务都不再依赖旧版本
→ 删除兼容代码Schema Registry 能检查结构兼容,但无法判断 score 从“本局分数”悄悄变成“赛季总分”这种语义破坏。事件契约需要所有者和评审。
错误处理按类别路由
反序列化失败、业务拒绝、依赖超时和代码缺陷需要不同处置:
| 错误 | 策略 |
|---|---|
| 短暂依赖失败 | 有上限的退避重试 |
| 永久 schema 错误 | 隔离并告警,不反复阻塞 |
| 重复消息 | 幂等返回成功 |
| 业务状态冲突 | 记录业务拒绝或进入修复流程 |
| 未知缺陷 | 保留上下文,限制重试,进入死信 |
下一课讨论发布订阅、流处理和完整事件链路的可观测性。
参考资料
- Apache Kafka, Design
- CloudEvents, Specification 1.0.2
- W3C, Trace Context
- Martin Fowler, What do you mean by “Event-Driven”?