浅谈领域事件及其应用

前言:好久没更新博客啦。这阵子刚忙完,稍微空暇,就想分享下在开发中用过的领域事件。因为大家做微服务的,基本上都会用DDD去进行领域驱动设计。而领域事件是领域模型里一个很重要的概念。下面开搞,放心,不只是理论哦,有我实战的可运行demo,你可以照着这个模板去开发,领域的对象可以自己去抽象和建模哦~~ 阿信觉得这期是干货。前两节的理论部分借鉴了别的文章,但第三节开始全是个人工作中的实战,简化了核心代码,把核心框架抽成demo分享一下。

 

 

0.领域事件的优势

先说说领域事件的优势。让你明白为啥用它。事件驱动和观察者模式本质一样,事件驱动是观察者模式的经典实现。

事件驱动的好处:

1、 解耦,事件发布者和订阅者不需要预先知道彼此的存在。

2、 异步消息传递,业务逻辑和事件可以同步发生。

3、 多对多的交互,发布订阅模型。

 

1.领域事件定义

A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state change in the other model objects.

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。

 

针对官方释义,我们可以理出以下几个要点:

  • 领域事件作为领域模型的重要部分,是领域建模的工具之一。

  • 用来捕获领域中已经发生的事情。

  • 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件。

  • 领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。

简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。它的本质就是事件,不要将其复杂化。在DDD中,领域事件作为通用语言的一种,是为了清晰表述领域中产生的事件概念,帮助我们深入理解领域模型。

 

2.领域事件案例

举个栗子,拿一个订单系统来说,下单成功之后,后续的动作:需要更新订单状态为支付成功,扣减库存,通知用户交易成功。

在这个用例中,“订单支付成功”就是一个领域事件。

考虑一下,在你没有接触领域事件或EDA(事件驱动架构)之前,你会如何实现这个用例。肯定是简单直接的方法调用,在一个事务中分别去调用状态更新方法、扣减库存方法、发送用户通知方法。这无可厚非,毕竟之前都是这样干的。

那这样设计有什么问题?

  1. 试想一下,若现在要求支付成功后,需要额外发送一条付款成功通知到微信公众号,我们怎么实现?想必我们需要额外定义发送微信通知的接口并封装参数,然后再添加对方法的调用。这种做法虽然可以解决需求的变更,但很显然不够灵活耦合性强,也违反了OCP。

  2. 将多个操作放在同一个事务中,使用事务一致性可以保证多个操作要么全部成功要么全部失败。在一个事务中处理多个操作,若其中一个操作失败,则全部失败。但是,这在业务上是不允许的。客户成功支付了,却发现订单依旧为待付款,这会导致纠纷的。

  3. 违反了聚合的一大原则:在一个事务中,只对一个聚合进行修改。在这个用例中,很明显我们在一个事务中对订单聚合和库存聚合进行了修改。

那如何解决这些问题?我们可以借助领域事件的力量。

  1. 解耦,可以通过发布订阅模式,发布领域事件,让订阅者自行订阅;

  2. 通过领域事件来达到最终一致性,提高系统的稳定性和性能;

  3. 事件溯源

 

3.领域事件建模

抽象和建模能力是软工们必不可少的能力之一,这节针对上面的订单系统出个简单的模型设计。

抽象出如下对象:

  1. 事件源    entry

  2. 事件对象 domainMessage  = 事件类型 eventTopic + 事件源  entry

  3. 事件监听器   subscribe 处理事件

  4. 事件分发器 注册监听器  JvmEventConsumer 

  5. 生产消息 实时消费,producer中直接consume 或者 MQ的形式,异步消费

领域事件流程图

 

4.领域事件实战

大家想要的demo在这里,可以说是大厂的编程模板了,哈哈。每段源码都会贴,但想下载源码,暂时我不想让你们偷懒

 上面简单的流程图给大家看看模型的,这节直接上代码。先贴个我的类图

 

领域事件类图

 

补充:

事件的发布方式:

1. 发布订阅模式(本文采用的)

也有叫它观察者模式。其实区别不大。空了我会补上观察者模式和订阅模式的区别。

2.基于ThreadLocal的事件发布

3.MQ消息

事件的发布者生产MQ,消费去接受消费MQ。可以通过Mafka等MQ去实现的。

其实本文也是生产发布消息,只不过生产的是JVM的消息,然后消费者进程即产即消的方式去消费的。

这里事件的主题EventTopic先写两种:

  1. 支付
  2. 退款

下面是源码,包名都去掉了[Doge][Doge][Doge] ,入口是EventPublisher的Main函数

 


import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.util.UuidUtil;

import java.util.Date;

/**
 * 领域事件容器类
 *
 */
public class DomainMessage<T> {

    /***
     * msg的唯一标识
     * 常用语用于幂等校验
     */
    private String messageId = StringUtils.EMPTY;


    /**
     * 消息的topic
     * see @{EventTopic}
     */
    private EventTopic eventTopic;


    /**
     * 消息体
     */
    private T messageBody;


    /**
     * 消息的创建时间
     */
    private Date addTime = new Date();


    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    public void setEventTopic(EventTopic eventTopic) {
        this.eventTopic = eventTopic;
    }

    public void setMessageBody(T messageBody) {
        this.messageBody = messageBody;
    }

    public String getMessageId() {
        return messageId;
    }

    public EventTopic getEventTopic() {
        return eventTopic;
    }

    public T getMessageBody() {
        return messageBody;
    }

    public Date getAddTime() {
        return addTime;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof DomainMessage)) {
            return false;
        }

        if (this.messageId.equals(((DomainMessage) obj).messageId)) {
            return true;
        }
        return super.equals(obj);
    }

    public DomainMessage() {
    }

    public DomainMessage(T messageBody, EventTopic eventTopic) {
        this.messageBody = messageBody;
        this.eventTopic = eventTopic;
        this.messageId = UuidUtil.getTimeBasedUuid().toString();
    }

    @Override
    public int hashCode() {
        return messageId.hashCode();
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }


}



/**
 * 领域事件消费者
 *
 */
public interface EventConsumer<T> {

    /**
     * 消费领域事件消息
     *
     * @param domainMessage
     */
    void consume(DomainMessage<T> domainMessage);
}

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EventEntry {

    /**
     * 商品
     */
    private String shop;

    /**
     * 金额
     */
    private int amount;
}


/**
 * 领域事件生产者
 *
 */
public interface EventProducer<T> {

    /**
     * 生产DomainEvent事件
     *
     * @param
     */
    void produce(DomainMessage<T> domainMessage);

}


import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class EventPublisher {

    private static JvmEventProducer jvmEventProducer;

    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
        jvmEventProducer = context.getBean(JvmEventProducer.class);

        //发布一个支付成功的事件
        publishPayEvent();

        //发布一个退款成功的事件
        publishRefundEvent();
    }

    private static void publishPayEvent() {
        DomainMessage<EventEntry> message = new DomainMessage<>();
        message.setEventTopic(EventTopic.PAY);
        message.setMessageBody(
                EventEntry.builder()
                        .amount(100)
                        .build()
        );
        jvmEventProducer.produce(message);
    }

    private static void publishRefundEvent() {
        DomainMessage<EventEntry> message = new DomainMessage<>();
        message.setEventTopic(EventTopic.REFUND);
        message.setMessageBody(
                EventEntry.builder()
                        .amount(100)
                        .build()
        );
        jvmEventProducer.produce(message);
    }
}


/**
 * 消息订阅者
 *
 */
public interface EventSubscriber<T> {

    /**
     * 事件处理主函数
     *
     * @param domainMessage
     */
    void handlerEvent(DomainMessage<T> domainMessage);


    /**
     * 获取订阅者的订阅主题
     *
     * @return
     */
    Boolean isSubscribedTopic(EventTopic topic);
}


import org.springframework.stereotype.Component;


/**
 * 占位用
 */
@Component
public class EventSubscriberStub implements EventSubscriber<Void> {

    @Override
    public void handlerEvent(DomainMessage<Void> domainMessage) {
        //do nothing
    }

    @Override
    public Boolean isSubscribedTopic(EventTopic topic) {
        return false;
    }

}


/**
 * 事件主题
 */
public enum EventTopic {
    PAY("pay", "支付"),

    REFUND("refund", "退款"),;

    private EventTopic(String action, String desc) {
        this.action = action;
        this.desc = desc;
    }

    private String action;

    private String desc;

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}


import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 基于JVM的消费机,只适用于单JVM项目应用,消费本JVM产生的Event
 *
 */
@Component
public class JvmEventConsumer implements EventConsumer, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(JvmEventConsumer.class);

    @Resource
    private List<EventSubscriber> eventSubscribers;

    /**
     * 根据Topic注册信息或者EventSubscriber列表
     * 该列表在应用JVM实例初始化时初始化
     */
    private Map<EventTopic, List<EventSubscriber>> topicQueue = new HashMap<EventTopic, List<EventSubscriber>>();

    private final ExecutorService executorService = Executors.newFixedThreadPool(100);

    @Override
    public void consume(final DomainMessage domainMessage) {
        if (domainMessage != null) {
            if (MapUtils.isNotEmpty(topicQueue)) {
                List<EventSubscriber> topicList = topicQueue.get(domainMessage.getEventTopic());
                if (CollectionUtils.isNotEmpty(topicList)) {
                    for (final EventSubscriber s : topicList) {
                        try {
                            executorService.submit(new Runnable() {
                                @Override
                                public void run() {
                                    s.handlerEvent(domainMessage);
                                }
                            });
                        } catch (Exception e) {
                            LOGGER.error("EventSubscriber handler exception", e);
                        }
                    }
                }

            }
        }
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        try {
            //init rules
            for (EventTopic topic : EventTopic.values()) {
                for (EventSubscriber en : eventSubscribers) {
                    if(en.isSubscribedTopic(topic)){
                        if (topicQueue.containsKey(topic)) {
                            topicQueue.get(topic).add(en);
                        } else {
                            List<EventSubscriber> tempList = Lists.newArrayList(en);
                            topicQueue.put(topic, tempList);
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("queueInit exception", e);
        }

    }
}


import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 领域事件的生产者生产消息后,由同一个JVM实例的消费者消费;
 * 生产出来就同步消费掉,消息不做持久化和异步逻辑
 *
 */
@Service
public class JvmEventProducer implements EventProducer {

    @Autowired
    private JvmEventConsumer jvmEventConsumer;

    @Override
    public void produce(DomainMessage domainMessage) {
        Transaction transaction = Cat.newTransaction("JVMMessageProducer", domainMessage.getEventTopic().name());
        try {
            jvmEventConsumer.consume(domainMessage);
            transaction.setStatus(Transaction.SUCCESS);
        } finally {
            transaction.complete();
        }
    }

}


import com.google.common.collect.Lists;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @Desc 订单支付事件的订阅者
 */

@Component
public class OrderPayEventSubscriber implements EventSubscriber<EventEntry> {

    @Override
    public void handlerEvent(DomainMessage<EventEntry> domainMessage) {
        EventTopic topic = domainMessage.getEventTopic();
        EventEntry entry = domainMessage.getMessageBody();
        switch (topic) {
            case PAY:
                handlePay(entry);
                break;
            case REFUND:
                handleRefund(entry);
                break;
            default:
                break;
        }
    }

    @Override
    public Boolean isSubscribedTopic(EventTopic topic) {
        List<EventTopic> topics = Lists.newArrayList(EventTopic.PAY, EventTopic.REFUND);
        return topics.contains(topic);
    }

    private void handlePay(EventEntry entry) {
        System.out.println("更新订单状态为支付成功");
        reduceStock(entry);
    }

    private void handleRefund(EventEntry entry) {
        System.out.println("更新订单状态为支付失败");
        returnStock(entry);
    }

    private void reduceStock(EventEntry entry) {
        System.out.println("扣减库存成功");
        System.out.println("通知用户交易成功,您成功支付:" + entry.getAmount());
    }

    private void returnStock(EventEntry entry) {
        System.out.println("回退库存成功");
        System.out.println("通知用户退款成功,成功退款:" + entry.getAmount());
    }
}


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan(basePackages = "你的包的路径")
@Configuration
public class BeanConfig {
}

最后附上运行结果:

 

针对demo的解释:

1.BeanConfig 

是通过@Configuration和 @ComponentScan 扫面以上所有class所在的包,为了注入bean。通过XML配置的方式也可以。为了在main里可以注入需要的bean。

 

2.说一下JvmEventConsumer 实现了InitializingBean接口。

InitializingBean接口为bean提供了属性初始化后的处理方法,它只包括afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。spring在设置完属性之后就会调研afterPropertiesSet方法

 

关于spring初始化bean的方法:

1:spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用 (我们经常用xml配置bean时候 init-method="init")

2:实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖

3:如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。

 

好啦,看完这篇,你可以骚气的开发了。别总是把业务逻辑和事件耦合在一起,结合自己的业务场景,试试这花里胡哨却又实用的开发模式吧,解耦让你的代码更清爽,异步可以提升性能。只会无脑怼线程池去做异步操作的RD不是好RD哦。哈哈哈哈

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值