领域事件 disruptor 使用场景之实现Spring事件驱动模型 ApplicationEvent

前言

Disruptor 是一个开源的并发框架。由英国外汇交易公司LMAX开发的一个高性能队列,并且大大的简化了并发程序开发的难度,获得2011Duke’s程序框架创新奖。

假设场景

假设有这么一种业务场景,业务为【用户注册】处理完后,同时触发【邮件通知】业务、【赠送积分】业务的执行,在不利用MQ的情况下,会有什么样的解决思路?可能的解决思路有如下

  1. 业务【用户注册】处理后,开启线程处理【邮件通知】、【赠送积分】的业务
  2. 使用disruptor进行处理
  3. 生产消费者模式
  4. 观察者模式 … 解决的思路有很多。

​ 本文就介绍的是基于disruptor实现的,类似Spring事件驱动模型 ApplicationEvent,这里称为领域事件。事件驱动模式与观察者模式在某些方面极为相似;当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而事件驱动模式则可以与多个事件源关联。下面先看一下对于领域事件的介绍。

简介 Event Source 领域事件

  1. 领域驱动设计,基于LMAX架构。
  2. 单一职责原则,可以给系统的可扩展、高伸缩、低耦合达到极致。
  3. 异步高并发、线程安全的、使用disruptor环形数组来消费业务。可并发执行,性能超高,执行1000W次事件只需要1.1秒左右(这个得看你的电脑配置)。
  4. 使用事件消费的方式编写代码,使得业务在复杂也不会使得代码混乱,维护代码成本更低
  5. 可灵活的定制业务线程模型
  6. 插件形式提供事件领域,做到了可插拔,就像玩乐高积木般有趣。

领域事件的本质是对disruptor的封装使用。

使用场景

计算密集型的业务推荐使用,比如斗地主中的出牌,所有牌桌可使用同一个领域事件来处理,但牌局结算时如果涉及IO (类似 DB 的入库时 ),就可切换到其他的领域事件,这样就可以不阻塞出牌业务线程。

​ 一个领域事件可以看成是一个线程,那么也就是说,我们可以用一个出牌的领域事件,做计算密集型的出牌业务,用N个结算的领域事件来做结算业务。可以举例不太恰当,但大概就是这个意思。

​ 下面的示例中也介绍了其他的使用场景,类似Spring事件驱动模型 ApplicationEvent。

图解

领域事件消费

在使用的角度,程序员只需要关注两件事

  1. 定义业务数据载体(领域消息实体
  2. 处理该业务数据载体的事件 (领域事件)

示例

自定义领域消息实体

  1. 定义领域实体 – 并实现 Eo 接口
  2. Eo 接口是框架提供, 领域实体类用户随意编写, 建议以Eo结尾.
  3. 领域消息实体的对象字段随意定制 (根据你的业务)
/**
 * 领域消息 - 学生
 * <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();
    }
}

​ 当使用上领域事件后,可以在不改变原有业务代码,就可以添加将来新增的业务逻辑。如上面示例代码中的

  • 如果将来还需要记录学生今天上了什么课程,那么也是直接配置 (可扩展) 这里的业务代码无需任何改动(松耦合)
  • 如果将来又不需要给学生发送email的事件了,直接删除配置即可,这里还是无需改动代码。(高伸缩)

在回到开头的【用户注册】处理完后的业务。我们只需要预留一个【用户注册】的Eo,将来如果有新增业务如:

  • 【邮件通知】业务
  • 【赠送积分】业务

各种 XXX 等新业务,我们也不需要改动【用户注册】这块的代码。只要单一职责原则,我们的代码维护性会非常的高。

总结

​ 使用基于 disruptor 的领域驱动设计的领域事件,可以使得我们的代码有如下好处:

  1. 单一职责原则,可以给系统的可扩展、高伸缩、低耦合达到极致。
  2. 异步高并发
  3. 业务在复杂也不会使得代码混乱,维护代码成本更低
  4. 插件形式提供事件领域,做到了可插拔,就像玩乐高积木般有趣。

源码参考地址:light-domain-event 模块

java 网络游戏服务器框架-文档

java 网络游戏服务器框架-源码

最后

本文可以转载,但必须保留所有内容。

  • Trackback 关闭
  • 评论 (1)
    • tuanjie
    • 2022/06/02 1:33下午

    有个问题,如果事件发布后系统异常停机,有什么补偿措施吗?

return top