前言
Disruptor 是一個開源的併發框架。由英國外匯交易公司LMAX開發的一個高性能隊列,並且大大的簡化了併發程序開發的難度,獲得2011Duke’s程序框架創新獎。
假設場景
假設有這麼一種業務場景,業務爲【用戶註冊】處理完後,同時觸發【郵件通知】業務、【贈送積分】業務的執行,在不利用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<StudentEo> {
@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 的領域驅動設計的領域事件,可以使得我們的代碼有如下好處:
- 單一職責原則,可以給系統的可擴展、高伸縮、低耦合達到極致。
- 異步高併發
- 業務在複雜也不會使得代碼混亂,維護代碼成本更低
- 插件形式提供事件領域,做到了可插拔,就像玩樂高積木般有趣。
源碼參考地址:light-domain-event 模塊
最後
本文可以轉載,但必須保留所有內容。