淺談領域事件及其應用

前言:好久沒更新博客啦。這陣子剛忙完,稍微空暇,就想分享下在開發中用過的領域事件。因爲大家做微服務的,基本上都會用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哦。哈哈哈哈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章