加载中...

本篇要搞懂的 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 的设计(子类按需扩充属性方法,事件本身行为很少、实现简单):

DomainEvent 事件基类

运行机制案例:缴费通知单事件(投保 ↔ 收款)

把上面 5 个组件串成一次完整的运行(领域事件:缴费通知单已生成,后续操作:缴费):

缴费通知单事件运行机制

  1. 投保微服务应用服务,调领域服务 createPaymentNotice + createPaymentNoticeEvent,分别造出缴费通知单 + 事件(事件类继承 DomainEvent)。
  2. 用仓储把业务数据 + 事件数据一起持久化到本地投保库(同一本地事务,避开分布式事务)。
  3. 通过 CDC 或定时程序,从事件表捞增量事件,发布到消息中间件。
  4. 收款微服务在应用层订阅该事件主题,监听拿到数据后,把事件数据存到自己本地库。
  5. 收款微服务调领域服务 PayPremium,完成缴费。
  6. 事件结束(缴费完成后又会触发 缴费已完成 等新事件,机制同上)。

总结

领域事件是 DDD 里表示"领域中已经发生、且会触发后续操作"的对象。它的最大价值是解耦:发布方发完事件就不管了,不关心订阅方成没成功,从而切断领域模型 / 微服务之间的强依赖,并支持"一个发布方 N 个订阅方"——这是传统直接调用做不到的。

识别领域事件靠抓业务方"如果 A 发生则做 B"的句式。为什么用最终一致而非直接调用:因为一次事务只能改一个聚合,跨聚合 / 跨微服务若用直接调用就得上分布式事务,既伤性能又增耦合;改用"发事件 + 异步订阅",各服务独立、最终一致即可。

落地机制分两种场景:微服务内用事件总线(进程内,可不引中间件),微服务间用消息中间件(Kafka/RabbitMQ,异步)。完整架构由"事件构建发布、事件持久化、事件总线、消息中间件、事件接收处理"5 个组件构成;其中"业务与事件数据同库本地事务 + CDC 捞取发布"是规避分布式事务的关键手法。

一句话速记

聚合说"跨我用最终一致",领域事件就是那个最终一致的实现工具——发一条"已发生"的通知,让关心的人自己异步去接着干

思考题

  • 你手头的项目里,有哪些"A 发生后要做 B、C、D"的场景?这些 A 就是领域事件候选。
  • 这些后续动作里,哪些是必须当场完成(强一致、留在聚合内 / 直接调用),哪些晚几秒也没事(可以改成发事件、最终一致)?
  • 有没有"为了一个次要的后续动作(如加积分、发通知),把主流程(如下单)拖垮 / 拖慢"的地方?那里往往就是该引入领域事件解耦的点。
公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关