假设场景
假设有这么一种业务场景,业务为【用户注册】处理完后,同时触发【邮件通知】业务、【赠送积分】业务的执行,在不利用MQ的情况下,会有什么样的解决思路?可能的解决思路有如下
业务【用户注册】处理后,开启线程处理【邮件通知】、【赠送积分】的业务
使用disruptor进行处理
生产消费者模式
观察者模式 … 解决的思路有很多。
本文就介绍的是基于disruptor实现的,类似Spring事件驱动模型 ApplicationEvent,这里称为领域事件。事件驱动模式与观察者模式在某些方面极为相似;当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而事件驱动模式则可以与多个事件源关联。下面先看一下对于领域事件的介绍。
简介 Event Source 领域事件
领域驱动设计,基于LMAX架构。
单一职责原则,可以给系统的可扩展、高伸缩、低耦合达到极致。
异步高并发、线程安全的、使用disruptor环形数组来消费业务。可并发执行,性能超高,执行1000W次事件只需要1.1秒左右(这个得看你的电脑配置)。
使用事件消费的方式编写代码,使得业务在复杂也不会使得代码混乱,维护代码成本更低。
可灵活的定制业务线程模型
插件形式提供事件领域,做到了可插拔,就像玩乐高积木般有趣。
领域事件的本质是对disruptor的封装使用。
使用场景
计算密集型的业务推荐使用,比如斗地主中的出牌,所有牌桌可使用同一个领域事件来处理,但牌局结算时如果涉及IO (类似 DB 的入库时 ),就可切换到其他的领域事件,这样就可以不阻塞出牌业务线程。
一个领域事件可以看成是一个线程,那么也就是说,我们可以用一个出牌的领域事件,做计算密集型的出牌业务,用N个结算的领域事件来做结算业务。可以举例不太恰当,但大概就是这个意思。
下面的示例中也介绍了其他的使用场景,类似Spring事件驱动模型 ApplicationEvent。
图解
在使用的角度,程序员只需要关注两件事
定义业务数据载体(领域消息实体)
处理该业务数据载体的事件 (领域事件)
示例
自定义领域消息实体
定义领域实体 – 并实现 Eo 接口
Eo 接口是框架提供, 领域实体类用户随意编写, 建议以Eo结尾.
领域消息实体的对象字段随意定制 (根据你的业务)
/** * 领域消息 - 学生 * <pre> * 推荐定义领域事件实体类的时候都使用final * 避免某个领域事件对该实体进行数据修改 * </pre> */public record StudentEo(int id) implements Eo {}
record 是值类型,好像是 java14 的出的(具体忘记的),转为java类的大概意思就是类中声明了一个属性 id,并自动提供 getter 方法。
定义领域事件
用于处理 StudentEo 领域消息实体
// 事件处理类,事件消费, 实现领域事件消费接口。一个事件消费类只处理一件事件(单一职责原则)public final class StudentEmailEventHandler1 implements DomainEventHandler { @Override public void onEvent(StudentEo studentEo, boolean endOfBatch) { log.debug("给这个学生发送一个email消息: {}", studentEo); }}
测试用例
public class StudentDomainEventTest { DomainEventContext domainEventContext; @After public void tearDown() throws Exception { // 事件消费完后 - 事件停止 domainEventContext.stop(); } @Before public void setUp() { // ======项目启动时配置一次(初始化)====== // 领域事件上下文参数 DomainEventContextParam contextParam = new DomainEventContextParam(); // 配置一个学生的领域事件消费 - 给学生发生一封邮件 contextParam.addEventHandler(new StudentEmailEventHandler1()); // 配置一个学生的领域事件消费 - 回家 // contextParam.addEventHandler(new StudentGoHomeEventHandler2()); // 配置一个学生的领域事件消费 - 让学生睡觉 // contextParam.addEventHandler(new StudentSleepEventHandler3()); // 启动事件驱动 domainEventContext = new DomainEventContext(contextParam); domainEventContext.startup(); } @Test public void testEventSend() { // 这里开始就是你的业务代码 StudentEo studentEo = new StudentEo(1); /* * 发送事件、上面只配置了一个事件。 * 如果将来还需要给学生发送一封email,那么直接配置。(可扩展) * 如果将来还需要记录学生今天上了什么课程,那么也是直接配置 (可扩展) 这里的业务代码无需任何改动(松耦合) * 如果将来又不需要给学生发送email的事件了,直接删除配置即可,这里还是无需改动代码。(高伸缩) */ studentEo.send(); }}
当使用上领域事件后,可以在不改变原有业务代码,就可以添加将来新增的业务逻辑。如上面示例代码中的
在回到开头的【用户注册】处理完后的业务。我们只需要预留一个【用户注册】的Eo,将来如果有新增业务如:
各种 XXX 等新业务,我们也不需要改动【用户注册】这块的代码。只要单一职责原则,我们的代码维护性会非常的高。
总结
使用基于 disruptor 的领域驱动设计的领域事件,可以使得我们的代码有如下好处:
单一职责原则,可以给系统的可扩展、高伸缩、低耦合达到极致。
异步高并发
业务在复杂也不会使得代码混乱,维护代码成本更低
插件形式提供事件领域,做到了可插拔,就像玩乐高积木般有趣。
源码参考地址:light-domain-event 模块