本篇要搞懂的 3 件事:领域事件(Domain Event)是什么、怎么识别它、它靠什么机制落地(事件总线 + 消息中间件 + 最终一致性)。
承接前文:05 讲留了一个尾巴——“一次事务最多只能改一个聚合,跨聚合要用最终一致性”,但没说"最终一致"具体怎么实现。06 讲就是来填这个坑的:领域事件,就是实现最终一致性、给微服务解耦的那个东西。
实体 / 值对象(04) 基础积木
↓ 按业务规则组合
聚合(05):一次事务只改一个聚合,跨聚合要最终一致
↓ 那"最终一致"怎么做?
领域事件(06):用"发事件 + 异步订阅"实现跨聚合 / 跨微服务的最终一致 ← 本讲
领域事件是什么
一句话:领域事件 = "某件已经发生的事"的一条通知,发出去之后会触发别人做后续动作。
定义里抠三个关键词:
| 关键词 | 含义 |
|---|---|
| 已经发生 | 它是过去式,命名都是"XX 已完成 / 已生成 / 已支付",不是"请去做 XX" |
| 一条通知 | 它只是个消息,不强迫谁立刻做什么 |
| 触发后续 | 有人收到它,会接着干自己的活,从而形成完整业务闭环 |
🪞 辅助理解:你结婚登记完(事件:
婚姻已登记),这件事一旦发生会触发一连串后续——改身份证、改社保、通知亲友……但民政局不负责帮你改身份证,它只是"把事办了",后面各部门自己接着办。这就是"一个事件、解耦后续、各自处理"。
领域事件的三类典型形态(来自原文举例):
- 业务流程的一步:投保缴费完成 → 触发"投保单转保单"
- 批处理中发生的事:批处理生成季缴通知单 → 触发"发缴费邮件"
- 某事发生后的后续动作:密码连错三次 → 触发"锁定账户"
怎么识别领域事件
诀窍很实用:做用户旅程 / 场景分析时,抓业务方嘴里这种句式——
“如果发生……,则……” · “当做完……的时候,请通知……” · “发生……时,则……”
只要听到"A 发生之后,要去做 B",那个 A 大概率就是一个领域事件。
| 业务方原话 | 领域事件(A) | 触发的后续(B) |
|---|---|---|
| “缴费完成后,就把投保单转成保单” | 缴费已完成 |
生成保单 |
| “密码错三次,就锁账户” | 密码连续输错3次 |
锁定账户 |
| “批处理生成通知单后,通知用户缴费” | 季缴通知单已生成 |
发缴费邮件 |
为什么用最终一致性,而不是直接调用?
这是本讲最核心、也最容易绕的一点。用一个订单例子讲透。
场景:用户支付成功,系统要同时做两件事——① 把订单标成"已支付" ② 给用户加积分。
而订单和积分是两个不同的聚合(回忆 05 讲:一次事务只能改一个聚合)。
❌ 传统做法(直接调用 / 强一致):
支付成功 → 改订单状态 →【同一个事务里】调用积分服务加积分
↑ 若积分服务挂了 / 超时
→ 整个事务回滚 → 订单也变回"未支付"!
问题:用户明明付了钱,只因为积分服务抽风,订单就被回滚了。两个本该独立的事被死死绑在一起,一个倒下另一个陪葬——这就是"强依赖"。
✅ 领域事件做法(最终一致):
支付成功 → 改订单状态 → 发事件「订单已支付」→ 事务结束,订单这边稳了 ✅
↓ (异步)
积分服务收到事件 → 自己慢慢加积分
就算它暂时挂了,消息还在队列里,恢复后补上 → 最终也会加上 ✅
| 对比维度 | 直接调用(强一致) | 领域事件(最终一致) |
|---|---|---|
| 耦合 | 订单服务必须知道积分服务存在、直接调它 | 订单发完事件就不管了,根本不需要知道积分存在 |
| 可扩展 | 再加"发短信""送券"要改订单代码 | 谁想要谁自己来订阅事件,订单一行不改(一个发布方 N 个订阅方) |
| 健壮性 | 积分挂了,订单跟着回滚 | 积分挂了,不影响"订单已支付"这个既成事实 |
| 一致性 | 每一刻都对得上 | 可能晚几秒,但最终一定对得上 |
🪞 辅助理解:对照 05 讲的比喻——夫妻当场对账(同一聚合内强一致);邻居晚上算账对得上就行(跨聚合最终一致)。积分晚几秒到账,业务上完全可以接受。
一图对比两种方式:
❌ 直接调用(强一致 / 强耦合) ✅ 领域事件(异步 / 最终一致 / 解耦)
┌─────────────┐ ┌─────────────┐
一个大事务 │ 订单服务 │ │ 订单服务 │ 小事务,发完即结束 ✅
把两个绑死 │ 改订单状态 │ │ 改订单状态 │
│ │ 同步调用│ │ 发「订单已支付」事件
│ ▼ │ └──────┬──────┘
│ ┌────────┐ │ │ 丢进消息队列(Kafka…)
│ │积分服务│ │ 积分挂了 ▼ 之后就不管了
│ │ 加积分 │ │ → 整个事务回滚 ┌───────────┐
│ └────────┘ │ → 订单也变回未支付! │ 消息队列 │ 消息留存,挂了也丢不了
└─────────────┘ └─────┬─────┘
┌─────────────┼─────────────┐
加一个"发短信" ▼ ▼ ▼
就得改订单代码 😫 ┌────────┐ ┌────────┐ ┌────────┐
│积分服务│ │短信服务│ │送券服务│ ← 谁要谁订阅
└────────┘ └────────┘ └────────┘ 订单一行不改 😀
各自异步处理,挂了恢复后补上 → 最终一致
- 左边:两个聚合被一个大事务焊死,一个倒下另一个陪葬;想加新动作要改主流程代码。
- 右边:订单只管"发一条已发生的通知",一个发布方 N 个订阅方,各订阅方独立异步处理、互不拖累。
→ 这就是为什么 DDD 提倡:跨聚合 / 跨微服务尽量用领域事件的最终一致,避免分布式事务。分布式事务会拖慢性能、增加耦合,能不用就不用。
两种领域事件场景
领域事件按"发生在哪"分两类,实现方式差别很大:
| 类型 | 发生位置 | 实现方式 | 要不要消息中间件 |
|---|---|---|---|
| 微服务内(聚合之间) | 同一个进程内 | 事件总线(EventBus),可同步可异步;进程自身能控事务 | 不一定需要,看复杂度和收益权衡 |
| 微服务之间 | 跨限界上下文 / 领域模型 | 走消息中间件(Kafka、RabbitMQ),异步发布订阅 | 需要,这是常见的大头 |
🪞 辅助理解:微服务内发事件像同一栋楼里递个条子;跨微服务发事件像跨城市寄快递——后者必须靠"邮局"(消息中间件)中转,所以实现更重、要考虑的东西更多。
补充:微服务内若一个事件要同时更新多个聚合,按"一次事务只更一个聚合"原则,就要考虑引入事件总线;但事件总线会增加开发复杂度,需权衡。实时性 + 强一致要求高的场景,也可以用应用服务做跨聚合编排(会用到分布式事务)。
案例:保险承保流程(事件接力)
一个保单的生成,跨了多个微服务,靠领域事件一棒接一棒地驱动流程往前走:
客户购买保险 → 录入保单 → 生成投保单 → 启动缴费
①投保微服务 发「缴费通知单已生成」→ 收款微服务订阅,去缴费
②收款微服务 发「缴费已完成」 → 投保微服务订阅,把投保单转成保单
③投保微服务 发「保单已生成」 → 保单微服务订阅,保存保单
④保单微服务 并发发出一堆事件 → 佣金、收付费、再保、财务…… 各自接着干
关键观察:第 ② 步里,原来的订阅方(收款)变成了发布方,原来的发布方(投保)变成了订阅方——发布/订阅的角色是随流程动态切换的。整条链路没有谁强依赖谁,全靠事件异步推动,这就是"事件驱动 + 解耦"。
原文完整流程图(投保→收款→保单→佣金/收付/再保→财务,每段都经 MQ 中转;7/9/13 是保单保存后并发触发的事件):
领域事件总体架构(5 个组件)
一个完整的领域事件机制,由这 5 块拼起来:
看图抓主线:聚合 A 发布 → 事件总线(微服务内直接分发给聚合 B;微服务外则落库 + 异步发 MQ)→ 消息中间件 MQ → 外部微服务 N 订阅处理。中间那块"事件数据持久化"就是规避分布式事务的关键。
| 组件 | 干什么 | 要点 |
|---|---|---|
| 1. 事件构建和发布 | 造出事件实体并发出去 | 事件 = 基本属性(全局唯一ID、发生时间、类型、事件源)+ 业务属性(事件发生那一刻的业务数据快照)。统一继承基类 DomainEvent。业务数据发生后不再改,可序列化成值对象存 |
| 2. 事件数据持久化 | 把事件存下来 | 用于对账、审计;中间件/订阅方宕机恢复后能续上,保证最终一致。两种存法:①存本地业务库的事件表(本地事务即可,推荐)②存共享事件库(跨库,需分布式事务,伤性能) |
| 3. 事件总线 EventBus | 微服务内分发事件 | 进程内模型,遍历订阅者列表。内部订阅者直接分发;外部订阅者先存事件表再异步发到消息中间件 |
| 4. 消息中间件 | 微服务间传事件 | Kafka / RabbitMQ 等,实现跨服务发布订阅 |
| 5. 事件接收和处理 | 订阅方干活 | 应用层监听消息队列 → 持久化事件数据 → 在领域服务里处理后续业务 |
💡 为什么要"先存事件表,再发消息":这是为了避免分布式事务。业务数据和事件数据都写进同一个本地库(一个本地事务搞定),再由定时程序 / 数据库日志捕获(CDC)把事件捞出来发到中间件。这样既保证了"业务和事件不会一个成功一个失败",又绕开了昂贵的分布式事务。
事件基类 DomainEvent 的设计(子类按需扩充属性方法,事件本身行为很少、实现简单):
运行机制案例:缴费通知单事件(投保 ↔ 收款)
把上面 5 个组件串成一次完整的运行(领域事件:缴费通知单已生成,后续操作:缴费):
- 投保微服务应用服务,调领域服务
createPaymentNotice+createPaymentNoticeEvent,分别造出缴费通知单 + 事件(事件类继承DomainEvent)。 - 用仓储把业务数据 + 事件数据一起持久化到本地投保库(同一本地事务,避开分布式事务)。
- 通过 CDC 或定时程序,从事件表捞增量事件,发布到消息中间件。
- 收款微服务在应用层订阅该事件主题,监听拿到数据后,把事件数据存到自己本地库。
- 收款微服务调领域服务
PayPremium,完成缴费。 - 事件结束(缴费完成后又会触发
缴费已完成等新事件,机制同上)。
总结
领域事件是 DDD 里表示"领域中已经发生、且会触发后续操作"的对象。它的最大价值是解耦:发布方发完事件就不管了,不关心订阅方成没成功,从而切断领域模型 / 微服务之间的强依赖,并支持"一个发布方 N 个订阅方"——这是传统直接调用做不到的。
识别领域事件靠抓业务方"如果 A 发生则做 B"的句式。为什么用最终一致而非直接调用:因为一次事务只能改一个聚合,跨聚合 / 跨微服务若用直接调用就得上分布式事务,既伤性能又增耦合;改用"发事件 + 异步订阅",各服务独立、最终一致即可。
落地机制分两种场景:微服务内用事件总线(进程内,可不引中间件),微服务间用消息中间件(Kafka/RabbitMQ,异步)。完整架构由"事件构建发布、事件持久化、事件总线、消息中间件、事件接收处理"5 个组件构成;其中"业务与事件数据同库本地事务 + CDC 捞取发布"是规避分布式事务的关键手法。
一句话速记
聚合说"跨我用最终一致",领域事件就是那个最终一致的实现工具——发一条"已发生"的通知,让关心的人自己异步去接着干。
思考题
- 你手头的项目里,有哪些"A 发生后要做 B、C、D"的场景?这些 A 就是领域事件候选。
- 这些后续动作里,哪些是必须当场完成(强一致、留在聚合内 / 直接调用),哪些晚几秒也没事(可以改成发事件、最终一致)?
- 有没有"为了一个次要的后续动作(如加积分、发通知),把主流程(如下单)拖垮 / 拖慢"的地方?那里往往就是该引入领域事件解耦的点。




