跳转到内容

领域事件(Domain Event)

用途:跨聚合、跨上下文协调变更,或需要审计“发生过什么”时查阅

领域事件是对业务上已经发生的事实的不可变描述,命名使用过去式(OrderPaid、ShipmentArrived、PolicyCancelled)。它声明“发生了什么”,不声明“该做什么”。

三个必备要素:事实(已发生)、时间(何时)、上下文(哪个聚合/标识)。事件一旦产生不可修改,纠正只能靠新事件。

聚合之间不允许放在同一事务里改,跨聚合与跨上下文的一致性需要一种解耦的传播机制。同步调用会把调用方与接收方的可用性、发布节奏绑在一起;事件把这种耦合降级为“契约耦合”,允许各自演进与延迟一致。

概念 语义 归属 例子
领域事件 业务事实 领域层 PaymentConfirmed
命令(Command) 请求执行的动作 应用层入口 PayOrder
集成消息 为下游系统准备的传输契约 基础设施/适配层 含外部字段的 payment-confirmed.v2 JSON
变更数据捕获(CDC) 数据层的变化流 基础设施 binlog 行变更

最容易出的错是把领域事件直接当作集成消息发出去:内部模型细节外泄,之后任何字段调整都变成破坏性变更。正确做法是内部领域事件 → 发布语言(显式 Schema)→ 外部消息。

事件传递只有三种真实保证,工程上必须说清用了哪种:

语义 含义 代价
最多一次 可能丢,不重复 需要兜底对账
至少一次 不丢,可能重复 消费方必须幂等
恰好一次 端到端精确一次 只能靠“生产端可靠 + 消费端幂等”组合逼近

Outbox 模式是落地至少一次的标准做法:业务数据与事件在同一本地事务内落库(事件表与业务表同库),再由后台任务或 CDC 投递到消息中间件,投递成功后标记。它解决“事务提交了但事件没发出去”以及“发了但事务回滚”两个经典缺陷。

开源参考:Debezium(CDC 投递 Outbox)、Apache Kafka / RocketMQ(事件通道)、Axon Framework(事件存储与订阅)、Spring Modulith(模块内事件与文档化)。

  • 事件 Schema 独立版本管理(Avro / Protobuf / JSON Schema),新增字段向后兼容,删除或改语义必须升版本。
  • 保留期与重放能力要匹配:事件可重放意味着订阅者必须能处理历史版本。
  • 事件目录(catalog)与负责人登记,避免“没人知道谁在消费这个事件”。
  • 聚合:聚合状态变更后产出事件;事件是跨聚合协调的唯一常规手段。
  • 最终一致:事件驱动必然带来延迟一致,需要补偿与对账兜底。
  • 事件溯源:把事件作为唯一真相来源、状态由事件重建,是更强的架构风格,成本显著更高,只在有审计/时间旅行需求时采用。
  • CQRS:写侧产出事件驱动读侧投影,两者常搭配但可独立使用。
误区 修正
事件名用动词原形(PayOrder) 那是命令;事件用过去式
事件体塞满内部字段与快照 只带订阅者决策所需的最小事实(ID + 关键值)
认为“用了事务消息所以绝对一致” 仍需消费幂等与对账,否则只是把丢失概率降低
事件链路过长无人负责 事件目录 + 每条边有负责人,链路深度设上限
用事件替代所有查询 读需求走查询服务,不要靠订阅一堆事件拼状态
  • Eric Evans《领域驱动设计》(领域事件原始表述)
  • Vaughn Vernon《实现领域驱动设计》第 4 章
  • Martin Fowler 关于 Event-Driven Architecture 与 Event Sourcing 的公开文章
  • Chris Richardson《微服务架构设计模式》第 8、11 章(Outbox、SAGA 与事件)