跳到内容

第 6 卷 / 第 17 章 / 第 1 课

17.1 事件契约、投递语义与消费者幂等 ​

事件驱动架构用已发生的事实连接生产者和消费者。它减少时间上的直接耦合,却把契约、重复、顺序、延迟和恢复变成一等设计问题。

事件不是“异步的函数调用”。先区分命令与事件:

  • 命令表达期望:SettleMatch,通常有目标接收者,可能被拒绝;
  • 事件表达事实:MatchSettled,由事实所有者发布,可以有零到多个消费者。

把命令命名成过去式事件,会掩盖责任;把事件命名成 UpdateRanking,会让发布者替消费者决定动作。

事件包含足够而稳定的语义 ​

下面使用项目自定义信封,给出完整的示例事件 ID 和 W3C 追踪上下文。它不是 CloudEvents 格式;若采用 CloudEvents 1.0,应使用其必需属性 id、source、specversion、type,并按扩展规则承载业务元数据。

json
{
  "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 消费者,照样会被一个“只是新增”的字段卡住。

重命名、改变类型、删除字段或复用旧字段表达新含义属于破坏性变更,应采用新版本并经历:

text
消费者先支持新旧版本
→ 生产者发布新版本
→ 检查线上消费、保留消息和重放任务都不再依赖旧版本
→ 删除兼容代码

Schema Registry 能检查结构兼容,但无法判断 score 从“本局分数”悄悄变成“赛季总分”这种语义破坏。事件契约需要所有者和评审。

错误处理按类别路由 ​

反序列化失败、业务拒绝、依赖超时和代码缺陷需要不同处置:

错误策略
短暂依赖失败有上限的退避重试
永久 schema 错误隔离并告警,不反复阻塞
重复消息幂等返回成功
业务状态冲突记录业务拒绝或进入修复流程
未知缺陷保留上下文,限制重试,进入死信

下一课讨论发布订阅、流处理和完整事件链路的可观测性。

参考资料 ​

Built with VitePress | Software Systems Atlas