Redian新闻
>
解密DDD:领域事件--系统解耦的终极武器

解密DDD:领域事件--系统解耦的终极武器

公众号新闻

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn

来源:geekhalo


1. 应用场景

假如你是订单服务的一名研发,正在开发支付成功这个业务功能,在深度学习 DDD 后,你写出了一组漂亮的代码。

@Transactional  
public void paySuccess(Long orderId){  
  
    // 1. 获取并验证订单聚合根有效性  
    Order order = this.orderRepository.getById(orderId);  
    if (order == null){  
        throw new IllegalArgumentException("订单不存在");  
    }  
  
    // 2. 修改价格  
    order.paySuccess();  
  
    // 3. 保存 Order 聚合  
    this.orderRepository.save(order);  
  
    // 4. 通知物流服务进行发货  
}  

成功上线后,系统运行稳定。随后,你又陆续接到更多需求,比如:

  1. 触达通知:支付成功后,需要向用户发送触达短信,告知用户已经完成支付;
  2. 清理购物车:用户成功购买商品后,把该商品从购物车中移除;
  3. 确认优惠券:如果用户购买时使用了优惠券,支付成功后调用优惠券服务标记优惠券已经被使用;
  4. 风控管理:完成支付后,调用风控系统提交订单数据,以便对当前交易进行风险评估;
  5. …..

更多的需求还在路上,此时原本漂亮的代码已经逐渐失控,变得有些面目全非:

@Transactional  
public void paySuccess(Long orderId){  
  
    // 1. 获取并验证订单聚合根有效性  
    Order order = this.orderRepository.getById(orderId);  
    if (order == null){  
        throw new IllegalArgumentException("订单不存在");  
    }  
  
    // 2. 修改价格  
    order.paySuccess();  
  
    // 3. 保存 Order 聚合  
    this.orderRepository.save(order);  
  
    // 4. 通知物流服务进行发货  
  
    // 5. 为用户发生触达短信  
    // 发送触达短信逻辑  
  
    // 6. 清理购物车  
  
    // 7. 使用优惠券,更新优惠券状态  
  
    // 8. 提交风控管理  
  
    // 其他代码  
  
}  

一些问题慢慢的浮现出来:

  1. 代码极速腐化:paySuccess 代码越来越多,想调整逻辑,需要从头看到尾,一不小心就会出错;
  2. 事务压力变大:方法越来越长,事务边界越来越大,占用数据库连接的时间越来越长,系统性能快速下降;
  3. 依赖越来越复杂:OrderApplicationService 实现类中,产生了很对外部依赖,比如物流、短信、购物车、优惠券、风控等;

前期这些问题你可能并不在意,直到有一天出现线上问题:

  1. 三方短信通道出现问题,影响订单支付!
  2. 购物车服务抖动,订单状态仍旧是待支付!
  3. 大数据风控服务上线,订单支付功能出现短时间不可用!

聪明的你为了避免别人的服务到影响自己,悄悄的在每个业务调用时增加了 try-catch,但腐化仍旧在延续……

如果你也意识到这个问题,那正是引入领域事件的好时机。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 领域事件

领域事件是领域模型的重要组成部分,用于表示在领域中发生的一些重要的业务事情或者状态变化,它用来捕获领域中的一些变更,记录事件发生时的业务状态,并将这些数据传输到订阅方,以开展后续业务操作。

领域事件有以下一些特点:

  1. 不可变性:领域事件表示已经发生的某种事实,该事实在发生后便不会改变,通常将其建模为值对象;
  2. 解耦系统:领域事件是事件驱动的核心组成部分,用于解耦系统中的各个部分,使得系统变得更加灵活、可扩展。通过发布订阅模式,发布领域事件,让订阅者自行订阅,从而达到解耦的目的;
  3. 最终一致性:通过领域事件来达到最终一致性,提高系统的稳定性和性能;

领域事件分为内部领域事件和外部领域事件,想搞清楚两者的区别,需要先回顾下“六边形架构”:

  1. 内六边形为领域模型,承载业务逻辑,内部领域事件应用于内六边形,主要用于服务或组件内部,在同一个服务、应用或限界上下文内实现解耦。
  2. 外六边形为基础设施,承载技术复杂性,外部领域事件应用于外六边形。用于实现跨服务、应用或限界上下文之间的通信,主要用于在微服务架构中实现解耦,或者在不同子域或限界上下文之间传播信息。

2.1. 内部领域事件

内部领域事件的主要目标是在领域间传播信息,以实现业务逻辑的分离和职责隔离。

内部领域事件通常使用同步或异步的方式在内存中传播。例如,在Java Spring中,可以使用ApplicationEventPublisher和@EventListener实现同步或异步的内部领域事件,这些事件不会跨服务或应用传播。

内部领域事件工作在内存中,在设计时需要注意以下几点:

  1. 直接使用 DDD 模型,无需转化为 DTO:所有操作都是在内存中完成,无需考虑对象粒度问题,直接使用即可,没有性能开销;
  2. 包含上下文的基础信息:通常包含事件发生的时间、事件类型、事件源和与事件相关的任何其他数据;
  3. 保持事件处理器职责单一:事件发布者与事件处理器之间为一对多的关系,事件处理器本身就是一个极佳的扩展点,不要为了减少事件处理器的数量而将逻辑耦合并到同一个处理器;
  4. 错误处理和重试策略:为了确保事件处理的可靠性和健壮性,在实现事件监听器时,要考虑到可能的错误场景,并设计相应的异常处理和重试策略;
  5. 同步或异步处理:根据业务需求决定事件是同步还是异步处理。同步意味着在发布事件后,事件处理器将立即执行,而发布者将等待其完成。异步意味着发布者将立即返回,事件处理将在另一个线程中进行。在考虑使用哪种方式时,需充分考虑资源竞争、锁定、超时等;

Spring Event 是内部领域事件落地的一把利器,稍后进行详解。

2.2. 外部领域事件

外部领域事件的主要目标是在跨服务或子域实现分布式的业务逻辑和系统间解耦。

外部领域事件通常使用消息队列(如Rocketmq、Kafka等)实现异步的跨服务传播。

外部领域事件工作在消息中间件之上,在设计时需要注意以下几点:

  1. 定制化 DTO:外部领域事件基于消息队列进行传播,对于庞大且数据巨大的领域对象非常不友好,同时为了防止内部概念的外泄,无法直接使用,需要对领域事件进行自定义;
  2. 事件序列化和反序列化:设计事件的序列化和反序列化机制,以便在不同系统之间传输和处理。常用的序列化格式包括JSON、XML、和二进制序列化,如Avro、Protobuf等,需要充分考虑消息兼容问题;
  3. 事件发布和订阅:选择一个支持可靠、高性能传输的消息中间件。例如,Kafka、RocketMQ等;
  4. 共享事件契约:契约包括:mq集群、topic、tag、Message 定义、Sharding Key 等;
  5. 错误处理和重试策略:和处理内部领域事件相似,需要考虑外部领域事件可能出现的错误,并设计相应的重试策略。特别是网络传输过程中可能出现的丢失、重复或延迟问题,需要设计相应的幂等操作、消息去重和顺序保证等措施;

消息中间件是 外部领域事件 落地的关键技术,由于篇幅原因,在此不做过多解释。稍后会有文章进行详解。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. Spring  Event 机制

Spring Event 是 Spring Framework 中的一个模块,帮助在应用程序中实现事件驱动。它主要用于组件之间同步/异步通信,解耦事件发布者和事件消费者。

使用 Spring Event 包括以下步骤:

  1. 定义事件:创建一个事件类,该类封装与特定事件相关的数据;
  2. 创建事件监听器:定义一个或多个事件监听器,在监听器中,处理特定类型的事件;
  3. 发布事件:调用ApplicationEventPublisher方法向外发布事件;

在 Spring 中,事件的处理器可以通过三种方式来实现:

  1. 基于接口的事件处理:通过实现 ApplicationListener 接口并重写 onApplicationEvent 方法来处理事件;
  2. 基于注解的事件处理:通过在方法上添加 @EventListener 或 @TransactionEventListener 注解来处理事件,可以指定事件的类型以及监听的条件等;
  3. 基于异步事件处理:通过使用 @Async 注解来异步处理事件,可以提高应用程序的响应速度;

3.1. 基于接口的事件处理

由于与 Spring 存在强耦合,现在已经很少使用,可以直接跳过。

下面是一个基于接口的事件处理的示例代码:

@Component  
public class MyEventListener implements ApplicationListener<MyEvent{  
    @Override  
    public void onApplicationEvent(MyEvent event) {  
        // 处理事件  
        System.out.println("Received event: " + event.getMessage());  
    }  
}  
  
public class MyEvent {  
    private String message;  
  
    public MyEvent(String message) {  
        this.message = message;  
    }  
  
    public String getMessage() {  
        return message;  
    }  
}  
  
@Component  
public class MyEventPublisher {  
    @Autowired  
    private ApplicationEventPublisher eventPublisher;  
  
    public void publishEvent(String message) {  
        MyEvent event = new MyEvent(message);  
        eventPublisher.publishEvent(event);  
    }  
}  

在这个示例中,MyEvent 是一个自定义的事件类,MyEventListener 是一个实现了 ApplicationListener 接口的监听器,用于处理 MyEvent 事件,MyEventPublisher 是用于发布事件的类。

当应用程序调用 MyEventPublisher 的 publishEvent 方法时,会触发一个 MyEvent 事件,MyEventListener 中的 onApplicationEvent 方法将被自动调用,从而处理这个事件。

3.2. 基于注解的事件处理

Spring 提供 @EventListener 和 @TransactionListener 两个注解以简化对事件的处理。

3.2.1. @EventListener

Spring 的 EventListener 监听器是一种相对于传统的事件监听方式更为简洁和灵活的事件机制。与传统的事件机制不同,EventListener 不需要显示地继承特定的事件接口,而是使用注解标识需要监听的事件类型,然后通过一个单独的监听器类处理所有类型的事件。

相比之下 EventListener 的优势主要有以下几点:

  1. 更加灵活:EventListener 不依赖于任何特定的事件接口,从而使得事件处理更加灵活,可以监听和处理任意类型的事件;
  2. 更加简洁:相比传统的事件监听方式,使用 EventListener 可以避免一系列繁琐的接口定义和实现,简化了代码结构,提升开发效率;
  3. 更加松耦合:EventListener 将事件发布方和事件处理方分离,遵循松耦合的设计原则,提高了代码的可维护性和扩展性;
  4. 更加可测试:由于 EventListener 可以监听和处理任意类型的事件,可以通过单元测试验证其功能是否正确,从而提高了测试的可靠性;

以下是一个简单的例子:

@Component  
public class MyEventListener{  
  
    @EventListener  
    public void onApplicationEvent(MyEvent event) {  
        // 处理事件  
        System.out.println("Received event: " + event.getMessage());  
    }  
}  
  
public class MyEvent {  
    private String message;  
  
    public MyEvent(String message) {  
        this.message = message;  
    }  
  
    public String getMessage() {  
        return message;  
    }  
}  
  
@Component  
public class MyEventPublisher {  
  
    @Autowired  
    private ApplicationEventPublisher eventPublisher;  
  
    public void publishEvent(String message) {  
        MyEvent event = new MyEvent(message);  
        eventPublisher.publishEvent(event);  
    }  
}  

相比基于接口的事件处理,EventListener 是一种更加简洁、灵活、松耦合、可测试的事件机制,能够有效地降低开发的复杂度,提高开发效率。

3.2.2. @TransactionEventListener

在 Spring 中,TransactionEventListner 和 EventListner 都是用于处理事件的接口。不同之处在于

  1. TransactionEventListner 是在事务提交后才会触发;
  2. 而 EventListner 则是在事件发布后就会触发;

具体来说,在使用 Spring 的声明式事务时,可以在事务提交后触发某些事件。这就是 TransactionEventListner 的应用场景。而 EventListner 则不涉及事务,可以用于在事件发布后触发一些操作。

下面是一个简单的示例,演示了如何使用 TransactionEventListner 和 EventListner:

@Component  
public class MyEventListener {  
  
    @EventListener  
    public void handleMyEvent(MyEvent event) {  
        // 处理 MyEvent  
    }  
  
    @TransactionalEventListener  
    public void handleMyTransactionalEvent(MyTransactionalEvent event) {  
        // 处理 MyTransactionalEvent  
    }  
}  
  
@Service  
public class MyService {  
  
    @Autowired  
    private ApplicationEventPublisher eventPublisher;  
  
    @Autowired  
    private MyRepository myRepository;  
  
    @Transactional  
    public void doSomething() {  
        // 做一些事情  
        MyEntity entity = myRepository.findById(1L);  
        // 发布事件  
        eventPublisher.publishEvent(new MyEvent(this, entity));  
        // 发布事务事件  
        eventPublisher.publishEvent(new MyTransactionalEvent(this, entity));  
    }  
}  

在这个例子中,MyEventListener 类定义了两个方法,handleMyEvent 和 handleMyTransactionalEvent,分别处理 MyEvent 和 MyTransactionalEvent 事件。其中,handleMyTransactionalEvent 方法用 @TransactionalEventListener 注解标记,表示它只会在事务提交后触发。

MyService 类中的 doSomething 方法使用 ApplicationEventPublisher 来发布事件。注意,它发布了两种不同类型的事件:MyEvent 和 MyTransactionalEvent。这两个事件会分别触发 MyEventListener 中的对应方法。

总的来说,Spring 的事件机制非常灵活,可以方便地扩展应用程序的功能。TransactionEventListner 和 EventListner 这两个接口的应用场景有所不同,可以根据实际需求选择使用。

3.3.基于异步事件处理

@Async是Spring框架中的一个注解,用于将一个方法标记为异步执行。使用该注解,Spring将自动为该方法创建一个新线程,使其在后台异步执行,不会阻塞主线程的执行。

在实际应用中,使用@Async可以大大提升应用的并发处理能力,使得系统能够更快地响应用户请求,提高系统的吞吐量。

@Async 和 @EventListener 或 @TransactionEventListener 注解在一起使用时,会产生异步的事件处理器。使用这种组合的方式,事件处理器会在单独的线程池中执行,以避免阻塞主线程。这种方式在需要处理大量事件或者事件处理器耗时较长的情况下非常有用,可以有效提升应用的性能和可伸缩性。同时,Spring 框架对这种方式也提供了完善的支持,可以方便地使用这种方式来实现异步事件处理。

下面是一个简单的示例代码,演示了如何在 Spring 中使用 @Async 和 @EventListener 一起实现异步事件处理:

@Component  
public class ExampleEventListener {  
  
    @Async  
    @EventListener  
    public void handleExampleEvent(ExampleEvent event) {  
        // 在新的线程中执行异步逻辑  
        // ...  
    }  
}  

在这个示例中,ExampleEventListener 类中的 handleExampleEvent 方法使用了 @Async 和 @EventListener 注解,表示这个方法是一个异步事件监听器。当一个 ExampleEvent 事件被触发时,这个方法会被异步地执行。在这个方法中,可以执行任何异步的逻辑处理,比如向队列发送消息、调用其他服务等。

备注:在使用 @Async 时,需要根据业务场景对线程池进行自定义,以免出现资源不够的情况(Spring 默认使用单线程处理@Async异步任务)

4. Spring Event 应用场景分析

综上所述,当领域事件发出来之后,不同的注解会产生不同的行为,简单汇总如下:

4.1. @EventListener

特点:

  1. 顺序执行。调用 publish(Event) 后,自动触发对 @EventListner 注释方法的调用
  2. 同步执行。使用主线程执行,方法抛出异常会中断调用链路,会触发事务的回归

应用场景:

  1. 事务消息表。在同一事务中完成对业务数据和消息表的修改
  2. 业务验证。对业务对象进行最后一次验证,如果验证不通过直接抛出异常中断数据库事务
  3. 业务插件。在当前线程和事务中执行插件完成业务扩展

4.2. @TransactionEventListener

特点:

  1. 事务提交后执行。调用 publish(Event) 时,只是向上下文中注册了一个回调器,并不会立即执行;只有在事务提交后,才会触发 @TransactionEventListner 注释方法的执行
  2. 同步执行。使用主线程执行,方法抛出异常会中断调用链路,但不会回归事务(事务已提交,没有办法进行回归)

应用场景:

  1. 数据同步。事务提交后,将变更同步到 ES 或 Cache
  2. 记录审计日志。只有在业务变更成功更新到数据库时才进行记录

备注:@TransactionEventLisnter 必须在事务上下文中,脱离上下文,调用不会生效

4.3. @EventListener + @Async

特点:

  1. 顺序执行。调用 publish(Event) 后,自动触发对 @EventListner 注释方法的调用
  2. 异步执行。使用独立的线程池执行任务,方法抛出异常对主流程没有任何影响

应用场景:

  1. 记日志明细日志,辅助排查问题

4.4. @TransactionEventListener + @Async

特点:

  1. 事务提交后执行。调用 publish(Event) 时,只是向上下文中注册了一个回调器,并不会立即执行;只有在事务提交后,才会触发对 @TransactionEventListner 注释方法的调用
  2. 异步执行。使用独立的线程池执行任务,方法抛出异常对主流程没有任何影响

应用场景:

异步处理。记录操作日志,异步保存数据等

备注:@TransactionEventLisnter 必须在事务上下文中,脱离上下文,调用不会生效

5. 小结

领域事件是系统中的解耦利器,包括:

  1. 内部事件 完成 领域模型内各组件间的解耦;
  2. 外部事件 完成 领域服务间的解耦;

Spring Event 是实现内部领域事件解耦的利器,基于 事件监听注解 和 同步/异步 两组注解的组合为不同的应用场景提供不同的支持。

外部领域事件 强依赖于消息中间件的使用,稍后会有文章进行详解。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
《江山情》&《壁上观》解密deepin-IDE:如何实现简单灵活的调试技术?北纬47°,藏着人类的终极浪漫《额尔古纳河右岸》:关于爱恨情仇的终极真相人类的终极定律:“薛定谔的猫”!颠覆《时间简史》,霍金的终极理论面世从你的手指流出了一个我买明年送今年!Wonderland季票优惠延至9月5日:一篇看懂饭卡+停车的终极deal聊聊俺收藏的老连环画:聊斋志异知识工作者的终极梦想,可能是拥有一个“第二大脑”时间管理的终极目的:高效率,慢生活没有魔法,只有对耐心与目标感的终极考验房地产的终极大招!存量房贷下调!接着加杠杆!国汽智控ICVOS,双解耦操作系统助力车企升级|年度好产品入围公示试吃超市6个品牌、30多种冰淇淋,我的终极大测评!昆虫变态——自然界中的终极变身魔法透过炒作看本质,谁是人工智能的终极赢家 | 巴伦封面OpenAI神秘Q*项目解密!诞生30+年「Q学习」算法引全球网友终极猜想人类追求的终极能源,就藏在这种恐怖的武器里电磁武器和激光武器,谁能先加入中国武器库,成为新一代定海神针女生的终极梦想:海蓝之谜,这套真的顶!人类的终极形态就是程心“又破又旧”的侘寂风,却是无数人的终极梦想未来机器人的终极形态是什么?DDD 对决:事务脚本 vs 领域模型,哪个才是业务优化的终极方案?安史之乱(6)香积寺会战,盛唐武德的终极汇报演出(万字篇)惠誉调低美国信用评级是冲击波吗?暴发户李湘,女人的终极梦想终于盼到了!语文阅读理解的终极解决方案,全是干货!LLM-based Agent 未必是通向AGI的终极路径?[掌设] 复古游戏掌机的的终极形态Analogue Pocket(openFPGA)和终极烧录卡Krikzz-EverDrive两部门联手,2023年度智能制造系统解决方案揭榜挂帅工作启动中年妇女的终极目标:把老公熬成爹!打造指数投资系统解决方案提供商,鹏华Ashares团队再获市场认可5128 血壮山河之武汉会战 信(阳)罗(山)战役 1
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。