领域事件(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 与事件)