摘要:將領(lǐng)域中所發(fā)生的活動(dòng)建模成一系列離散事件。領(lǐng)域事件是領(lǐng)域模型的組成部分,表示領(lǐng)域中所發(fā)生的事情。創(chuàng)建領(lǐng)域事件事件命名在建模領(lǐng)域事件時(shí),我們應(yīng)該根據(jù)限界上下文中的通用語(yǔ)言來命名事件。
使用領(lǐng)域事件來捕獲發(fā)生在領(lǐng)域中的一些事情。
領(lǐng)域驅(qū)動(dòng)實(shí)踐者發(fā)現(xiàn)他們可以通過了解更多發(fā)生在問題域中的事件,來更好的理解問題域。這些事件,就是領(lǐng)域事件,主要是與領(lǐng)域?qū)<乙黄疬M(jìn)行知識(shí)提煉環(huán)節(jié)中獲得。
領(lǐng)域事件,可以用于一個(gè)限界上下文內(nèi)的領(lǐng)域模型,也可以使用消息隊(duì)列在限界上下文間進(jìn)行異步通信。
1 理解領(lǐng)域事件領(lǐng)域事件是領(lǐng)域?qū)<宜P(guān)心的發(fā)生在領(lǐng)域中的一些事件。
將領(lǐng)域中所發(fā)生的活動(dòng)建模成一系列離散事件。每個(gè)事件都用領(lǐng)域?qū)ο蟊硎?。領(lǐng)域事件是領(lǐng)域模型的組成部分,表示領(lǐng)域中所發(fā)生的事情。
領(lǐng)域事件的主要用途:
保證聚合間的數(shù)據(jù)一致性
替換批量處理
實(shí)現(xiàn)事件源模式
進(jìn)行限界上下文集成
2 實(shí)現(xiàn)領(lǐng)域事件領(lǐng)域事件表示已經(jīng)發(fā)生的某種事實(shí),該事實(shí)在發(fā)生后便不會(huì)改變。因此,領(lǐng)域事件通常建模成值對(duì)象。2.1 創(chuàng)建領(lǐng)域事件但,這也有特殊的情況,為了迎合序列化和反序列化框架需求,在建模時(shí),經(jīng)常會(huì)進(jìn)行一定的妥協(xié)。
在建模領(lǐng)域事件時(shí),我們應(yīng)該根據(jù)限界上下文中的通用語(yǔ)言來命名事件。
如果事件由聚合上的命令操作產(chǎn)生,通常根據(jù)該操作方法的名字來命名事件。事件名字表明聚合上的命令方法在執(zhí)行成功后的事實(shí)。即事件命名需要反映過去發(fā)生過的事情。
public class AccountEnabledEvent extends AbstractAggregateEvent{ public AccountEnabledEvent(Account source) { super(source); } }
事件的屬性主要用于驅(qū)動(dòng)后續(xù)業(yè)務(wù)流程。當(dāng)然,也會(huì)擁有一些通用屬性。
事件具有一些通用屬性,如:
唯一標(biāo)識(shí)
occurredOn 發(fā)生時(shí)間
type 事件類型
source 事件發(fā)生源(只針對(duì)由聚合產(chǎn)生的事件)
通用屬性可以使用事件接口來規(guī)范。
接口或類 | 含義 |
---|---|
DomainEvent | 通用領(lǐng)域事件接口 |
AggregateEvent | 由聚合發(fā)布的通用領(lǐng)域事件接口 |
AbstractDomainEvent | DomainEvent 實(shí)現(xiàn)類,維護(hù) id 和 創(chuàng)建時(shí)間 |
AbstractAggregateEvent | AggregateEvent 實(shí)現(xiàn)類,繼承子 AbstractDomainEvent,并添加 source 屬性 |
但,事件最主要的還是業(yè)務(wù)屬性。我們需要考慮,是誰(shuí)導(dǎo)致事件的發(fā)生,這可能涉及產(chǎn)生事件的聚合或其他參與該操作的聚合,也可能是其他任何類型的操作數(shù)據(jù)。
事件是事實(shí)的描述,本身不會(huì)有太多的業(yè)務(wù)操作。
領(lǐng)域事件通常被設(shè)計(jì)為不變對(duì)象,事件所攜帶的數(shù)據(jù)已經(jīng)反映出該事件的來源。事件構(gòu)造函數(shù)完成狀態(tài)初始化,同時(shí)提供屬性的 getter 方法。
這里需要注意的是事件唯一標(biāo)識(shí),通常情況下,事件是不可變的,那為什么會(huì)涉及唯一標(biāo)識(shí)的概念呢?
對(duì)于從聚合中發(fā)布出來的領(lǐng)域事件,使用事件的名稱、產(chǎn)生事件的標(biāo)識(shí)、事件發(fā)生的時(shí)間等足以對(duì)不同的事件進(jìn)行區(qū)分。但,這樣會(huì)增加事件比較的復(fù)雜性。
對(duì)于由調(diào)用方發(fā)布的事件,我們將領(lǐng)域事件建模成聚合,可以直接使用聚合的唯一標(biāo)識(shí)作為事件的標(biāo)識(shí)。
事件唯一標(biāo)識(shí)的引入,會(huì)大大減少事件比較的復(fù)雜性。但,其最大的意義在于限界上下文的集成。
當(dāng)我們需要將領(lǐng)域事件發(fā)布到外部的限界上下文時(shí),唯一標(biāo)識(shí)就是一種必然。為了保證事件投遞的冪等性,在發(fā)送端,我們可能會(huì)進(jìn)行多次發(fā)送嘗試,直至明確發(fā)送成功為止;而在接收端,當(dāng)接收到事件后,需要對(duì)事件進(jìn)行重復(fù)性檢測(cè),以保障事件處理的冪等性。此時(shí),事件的唯一標(biāo)識(shí)便可以作為事件去重的依據(jù)。
事件唯一標(biāo)識(shí),本身對(duì)領(lǐng)域建模影響不大,但對(duì)技術(shù)處理好處巨大。因此,將它作為通用屬性進(jìn)行管理。2.2 發(fā)布領(lǐng)域事件
我們?nèi)绾伪苊忸I(lǐng)域事件與處理者間的耦合呢?
一種簡(jiǎn)單高效的方式便是使用觀察者模式,這種模式可以在領(lǐng)域事件和外部組件間進(jìn)行解耦。
為了統(tǒng)一,我們需要定義了一套接口和實(shí)現(xiàn)類,以基于觀察者模式,完成事件的發(fā)布。
涉及接口和實(shí)現(xiàn)類如下:
接口或類 | 含義 |
---|---|
DomainEventPublisher | 用于發(fā)布領(lǐng)域事件 |
DomainEventHandlerRegistry | 用于注冊(cè) DomainEventHandler |
DomainEventBus | 擴(kuò)展自 DomainEventPublisher 和 DomainEventHandlerRegistry 用于發(fā)布和管理領(lǐng)域事件處理器 |
DefaultDomainEventBus | DomainEventBus 默認(rèn)實(shí)現(xiàn) |
DomainEventHandler | 用于處理領(lǐng)域事件 |
DomainEventSubscriber | 用于判斷是否接受領(lǐng)域事件 |
DomainEventExecutor | 用于執(zhí)行領(lǐng)域事件處理器 |
使用實(shí)例如 DomainEventBusTest 所示:
public class DomainEventBusTest { private DomainEventBus domainEventBus; @Before public void setUp() throws Exception { this.domainEventBus = new DefaultDomainEventBus(); } @After public void tearDown() throws Exception { this.domainEventBus = null; } @Test public void publishTest(){ // 創(chuàng)建事件處理器 TestEventHandler eventHandler = new TestEventHandler(); // 注冊(cè)事件處理器 this.domainEventBus.register(TestEvent.class, eventHandler); // 發(fā)布事件 this.domainEventBus.publish(new TestEvent("123")); // 檢測(cè)事件處理器是夠運(yùn)行 Assert.assertEquals("123", eventHandler.data); } @Value class TestEvent extends AbstractDomainEvent{ private String data; } class TestEventHandler implements DomainEventHandler{ private String data; @Override public void handle(TestEvent event) { this.data = event.getData(); } } }
在構(gòu)建完發(fā)布訂閱結(jié)構(gòu)后,需要將其與領(lǐng)域模型進(jìn)行關(guān)聯(lián)。領(lǐng)域模型如何獲取 Publisher,事件處理器如何進(jìn)行訂閱。
比較常用的方案便是將 DomainEventBus 綁定到線程上下文。這樣,只要是同一調(diào)用線程都可以方便的獲取 DomainEventBus 對(duì)象。
具體的交互如下:
DomainEventBusHolder 用于管理 DomainEventBus。
public class DomainEventBusHolder { private static final ThreadLocalTHREAD_LOCAL = new ThreadLocal (){ @Override protected DomainEventBus initialValue() { return new DefaultDomainEventBus(); } }; public static DomainEventPublisher getPubliser(){ return THREAD_LOCAL.get(); } public static DomainEventHandlerRegistry getHandlerRegistry(){ return THREAD_LOCAL.get(); } public static void clean(){ THREAD_LOCAL.remove(); } }
Account 的 enable 直接使用 DomainEventBusHolder 進(jìn)行發(fā)布。
public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); DomainEventBusHolder.getPubliser().publish(event); } } public class AccountEnabledEvent extends AbstractAggregateEvent{ public AccountEnabledEvent(Account source) { super(source); } }
AccountApplication 完成訂閱器注冊(cè)以及業(yè)務(wù)方法調(diào)用。
public class AccountApplication extends AbstractApplication { private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class); @Autowired private AccountRepository repository; public void enable(Long id){ // 清理之前綁定的 Handler DomainEventBusHolder.clean(); // 注冊(cè) EventHandler AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler(); DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler); OptionalaccountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 使用 DomainEventBusHolder 直接發(fā)布事件 account.enable(); repository.save(account); } } class AccountEnableEventHandler implements DomainEventHandler { @Override public void handle(AccountEnabledEvent event) { LOGGER.info("handle enable event"); } } }
先將事件緩存在實(shí)體中,在實(shí)體狀態(tài)成功持久化到存儲(chǔ)后,再進(jìn)行事件發(fā)布。
具體交互如下:
實(shí)例代碼如下:
public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); registerEvent(event); } }
Account 的 enable 方法,調(diào)用 registerEvent 對(duì)事件進(jìn)行注冊(cè)。
@MappedSuperclass public abstract class AbstractAggregateextends AbstractEntity implements Aggregate { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class); @JsonIgnore @QueryTransient @Transient @org.springframework.data.annotation.Transient private final transient List events = Lists.newArrayList(); protected void registerEvent(DomainEvent event) { events.add(new DomainEventItem(event)); } protected void registerEvent(Supplier eventSupplier) { this.events.add(new DomainEventItem(eventSupplier)); } @Override @JsonIgnore public List getEvents() { return Collections.unmodifiableList(events.stream() .map(eventSupplier -> eventSupplier.getEvent()) .collect(Collectors.toList())); } @Override public void cleanEvents() { events.clear(); } private class DomainEventItem { DomainEventItem(DomainEvent event) { Preconditions.checkArgument(event != null); this.domainEvent = event; } DomainEventItem(Supplier supplier) { Preconditions.checkArgument(supplier != null); this.domainEventSupplier = supplier; } private DomainEvent domainEvent; private Supplier domainEventSupplier; public DomainEvent getEvent() { if (domainEvent != null) { return domainEvent; } DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null; domainEvent = event; return domainEvent; } } }
registerEvent 方法在 AbstractAggregate 中,registerEvent 方法將事件保存到 events 集合,getEvents 方法獲取所有事件,cleanEvents 方法清理緩存的事件。
Application 實(shí)例如下:
@Service public class AccountApplication extends AbstractApplication { private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class); @Autowired private AccountRepository repository; @Autowired private DomainEventBus domainEventBus; @PostConstruct public void init(){ // 使用 Spring 生命周期注冊(cè)事件處理器 this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler()); } public void enable(Long id){ OptionalaccountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 將事件緩存在 account 中 account.enable(); repository.save(account); List events = account.getEvents(); if (!CollectionUtils.isEmpty(events)){ // 成功持久化后,對(duì)事件進(jìn)行發(fā)布 this.domainEventBus.publishAll(events); } } } class AccountEnableEventHandler implements DomainEventHandler { @Override public void handle(AccountEnabledEvent event) { LOGGER.info("handle enable event"); } } }
AccountApplication 的 init 方法完成事件監(jiān)聽器的注冊(cè),enable 方法在實(shí)體成功持久化后,將緩存的事件通過 DomainEventBus 實(shí)例 publish 出去。
通常情況下,領(lǐng)域事件是由聚合的命令方法產(chǎn)生,并在命令方法執(zhí)行成功后,進(jìn)行事件的發(fā)布。
有時(shí),領(lǐng)域事件并不是聚合中的命令方法產(chǎn)生的,而是由用戶所發(fā)生的請(qǐng)求產(chǎn)生。
此時(shí),我們需要將領(lǐng)域事件建模成一個(gè)聚合,并且擁有自己的資源庫(kù)。但,由于領(lǐng)域事件表示的是過去發(fā)生的事情,因此資源庫(kù)只做追加操作,不能對(duì)事件進(jìn)行修改和刪除功能。
例如,對(duì)用戶點(diǎn)擊事件進(jìn)行發(fā)布。
@Entity @Data public class ClickAction extends JpaAggregate implements DomainEvent { @Setter(AccessLevel.PRIVATE) private Long userId; @Setter(AccessLevel.PRIVATE) private String menuId; public ClickAction(Long userId, String menuId){ Preconditions.checkArgument(userId != null); Preconditions.checkArgument(StringUtils.isNotEmpty(menuId)); setUserId(userId); setMenuId(menuId); } @Override public String id() { return String.valueOf(getId()); } @Override public Date occurredOn() { return getCreateTime(); } }
ClickAction 繼承自 JpaAggregate 實(shí)現(xiàn) DomainEvent 接口,并重寫 id 和 occurredOn 方法。
@Service public class ClickActionApplication extends AbstractApplication { @Autowired private ClickActionRepository repository; @Autowired private DomainEventBus domainEventBus; public void clickMenu(Long id, String menuId){ ClickAction clickAction = new ClickAction(id, menuId); clickAction.prePersist(); this.repository.save(clickAction); domainEventBus.publish(clickAction); } }
ClickActionApplication 在成功保存 ClickAction 后,使用 DomainEventBus 對(duì)事件進(jìn)行發(fā)布。
2.3 訂閱領(lǐng)域事件由什么組件向領(lǐng)域事件注冊(cè)訂閱器呢?大多數(shù)請(qǐng)求,由應(yīng)用服務(wù)完成,有時(shí)也可以由領(lǐng)域服務(wù)進(jìn)行注冊(cè)。
由于應(yīng)用服務(wù)是領(lǐng)域模型的直接客戶,它是注冊(cè)領(lǐng)域事件訂閱器的理想場(chǎng)所,即在應(yīng)用服務(wù)調(diào)用領(lǐng)域方法之前,就完成了對(duì)事件的訂閱。
基于 ThreadLocal 進(jìn)行訂閱:
public void enable(Long id){ // 清理之前綁定的 Handler DomainEventBusHolder.clean(); // 注冊(cè) EventHandler AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler(); DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler); OptionalaccountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 使用 DomainEventBusHolder 直接發(fā)布事件 account.enable(); repository.save(account); } }
基于實(shí)體緩存進(jìn)行訂閱:
@PostConstruct public void init(){ // 使用 Spring 生命周期注冊(cè)事件處理器 this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler()); } public void enable(Long id){ Optional2.4 處理領(lǐng)域事件accountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 將事件緩存在 account 中 account.enable(); repository.save(account); List events = account.getEvents(); if (!CollectionUtils.isEmpty(events)){ // 成功持久化后,對(duì)事件進(jìn)行發(fā)布 this.domainEventBus.publishAll(events); } } }
完成事件發(fā)布后,讓我們一起看下事件處理。
我們通常將領(lǐng)域事件用于維護(hù)模型的一致性。在聚合建模中有一個(gè)原則,就是在一個(gè)事務(wù)中,只能對(duì)一個(gè)聚合進(jìn)行修改,由此產(chǎn)生的變化必須在獨(dú)立的事務(wù)中運(yùn)行。
在這種情況下,需要謹(jǐn)慎處理的事務(wù)的傳播性。
應(yīng)用服務(wù)控制著事務(wù)。不要在事件通知過程中修改另一個(gè)聚合實(shí)例,因?yàn)檫@樣會(huì)破壞聚合的一大原則:在一個(gè)事務(wù)中,只能對(duì)一個(gè)聚合進(jìn)行修改。
對(duì)于簡(jiǎn)單場(chǎng)景,我們可以使用特殊的事務(wù)隔離策略對(duì)聚合的修改進(jìn)行隔離。具體流程如下:
但,最佳方案是使用異步處理。及每一個(gè)定義方都在各自獨(dú)立的事務(wù)中修改額外的聚合實(shí)例。
事件訂閱方不應(yīng)該在另一個(gè)聚合上執(zhí)行命令方法,因?yàn)檫@樣將破壞“在單個(gè)事務(wù)中只修改單個(gè)聚合實(shí)例”的原則。所有聚合實(shí)例間的最終一致性必須通過異步方式處理。
詳見,異步處理領(lǐng)域事件。
批處理過程通常需要復(fù)雜的查詢,并且需要龐大的事務(wù)支持。如果在接收到領(lǐng)域事件時(shí),系統(tǒng)就立即處理,業(yè)務(wù)需求不僅得到了更快的滿足,而且杜絕了批處理操作。
在系統(tǒng)的非高峰時(shí)期,通常使用批處理進(jìn)行一些系統(tǒng)的維護(hù),比如刪除過期數(shù)據(jù)、創(chuàng)建新的對(duì)象、通知用戶、更新統(tǒng)計(jì)信息等。這些批處理往往需要復(fù)雜的查詢,并需要龐大的事務(wù)支持。
如果我們監(jiān)聽系統(tǒng)中的領(lǐng)域事件,在接收領(lǐng)域事件時(shí),系統(tǒng)立即處理。這樣,原本批量集中處理的過程就被分散成許多小的處理單元,業(yè)務(wù)需要也能更快的滿足,用戶可以可以及時(shí)的進(jìn)行下一步操作。
對(duì)于單個(gè)限界上下文中的所有領(lǐng)域事件,為它們維護(hù)一個(gè)事件存儲(chǔ)具有很多的好處。
對(duì)事件進(jìn)行存儲(chǔ)可以:
將事件存儲(chǔ)作為消息隊(duì)列來使用,然后將領(lǐng)域事件通過消息設(shè)施發(fā)布出去。
將事件存儲(chǔ)用于基于 Rest 的事件通知。
檢查模型命名方法產(chǎn)生結(jié)果的歷史記錄。
使用事件存儲(chǔ)來進(jìn)行業(yè)務(wù)預(yù)測(cè)和分析。
使用事件來重建聚合實(shí)例。
執(zhí)行聚合的撤銷操作。
事件存儲(chǔ)是個(gè)比較大的課題,將有專門章節(jié)進(jìn)行講解。
基于領(lǐng)域事件的限界上下文集成,主要由消息隊(duì)列和 REST 事件兩種模式。
在此,重心講解基于消息隊(duì)列的上下文集成。
在不同的上下文中采用消息系統(tǒng)時(shí),我們必須保證最終一致性。在這種情況下,我們至少需要在兩種存儲(chǔ)之間保存最終一致性:領(lǐng)域模型所使用的存儲(chǔ)和消息隊(duì)列所使用的持久化存儲(chǔ)。我們必須保證在持久化領(lǐng)域模型時(shí),對(duì)于的事件也已經(jīng)成功發(fā)布。如果兩種不同步,模型可能會(huì)處于不正確的狀態(tài)。
一般情況下,有三種方式:
領(lǐng)域模型和消息共享持久化存儲(chǔ)。在這種情況下,模型和事件的提交在一個(gè)事務(wù)中完成,從而保證兩種的一致性。
領(lǐng)域模型和消息由全局事務(wù)控制。這種情況下,模型和消息所用的持久化存儲(chǔ)可以分離,但會(huì)降低系統(tǒng)性能。
在領(lǐng)域持久化存儲(chǔ)中,創(chuàng)建一個(gè)特殊的存儲(chǔ)區(qū)域用于存儲(chǔ)事件(也就是事件存儲(chǔ)),從而在本地事務(wù)中完成領(lǐng)域和事件的存儲(chǔ)。然后,通過后臺(tái)服務(wù)將事件異步發(fā)送到消息隊(duì)列中。
一般情況下,第三種,是比較優(yōu)雅的解決方案。
在一致性要求不高時(shí),可以通過領(lǐng)域事件訂閱器直接向消息隊(duì)列發(fā)送事件。具體流程如下:
對(duì)一致性要求高時(shí),需要先將事件存儲(chǔ),然后通過后臺(tái)線程加載并分發(fā)到消息隊(duì)列。具體流程如下:
2.5 異步處理領(lǐng)域事件領(lǐng)域事件可以與異步工作流程協(xié)同,包括限界上下文間使用消息隊(duì)列進(jìn)行異步通信。當(dāng)然,在同一個(gè)限界上下文中,也可以啟動(dòng)異步處理流程。
作為事件的發(fā)布者,不應(yīng)關(guān)心是否執(zhí)行異步處理。異常處理是由事件執(zhí)行者決定。
DomainEventExecutor 提供對(duì)異步處理的支持。
DomainEventExecutor eventExecutor = new ExecutorBasedDomainEventExecutor("EventHandler", 1, 100); this.domainEventBus.register(AccountEnabledEvent.class, eventExecutor, new AccountEnableEventHandler());
異步處理,就意味著放棄數(shù)據(jù)庫(kù)事務(wù)的 ACID 特性,而選擇使用最終一致性。2.6 內(nèi)部事件與外部事件
使用領(lǐng)域事件時(shí)需要對(duì)事件進(jìn)行區(qū)分,以避免技術(shù)實(shí)現(xiàn)的問題。
認(rèn)識(shí)內(nèi)部事件和外部事件之間的區(qū)別至關(guān)重要。
內(nèi)部事件,是一個(gè)領(lǐng)域模型內(nèi)部的事件,不在有界上下文間進(jìn)行共享。
外部事件,是對(duì)外發(fā)布的事件,在多個(gè)有界上下文中進(jìn)行共享。
一般情況下,在典型的業(yè)務(wù)用例中,可能會(huì)有很多的內(nèi)部事件,而只有一兩個(gè)外部事件。
內(nèi)部事件存在于限界上下文內(nèi)部,受限界上下文邊界保護(hù)。
內(nèi)部事件被限制在單個(gè)有界上下文邊界內(nèi)部,所以可以直接引用領(lǐng)域?qū)ο蟆?/p>
public interface AggregateEvent> extends DomainEvent{ A source(); default A getSource(){ return source(); } }
比如 AggregateEvent 中的 source 指向發(fā)布該事件的聚合。
public class LikeSubmittedEvent extends AbstractAggregateEvent{ public LikeSubmittedEvent(Like source) { super(source); } public LikeSubmittedEvent(String id, Like source) { super(id, source); } }
LikeSubmittedEvent 類直接引用 Like 聚合。
外部事件存在于限界上下文間,被多個(gè)上下文共享。
一般情況下,外部事件,只作為數(shù)據(jù)載體存在。常常采用平面結(jié)構(gòu),并公開所有屬性。
@Data public class SubmittedEvent { private Owner owner; private Target target; }
SubmittedEvent 為扁平化結(jié)構(gòu),主要是對(duì)數(shù)據(jù)的封裝。
由于外部事件被多個(gè)上下文共享,版本管理就顯得非常重要,以避免重大更改對(duì)其服務(wù)造成影響。3 實(shí)現(xiàn)領(lǐng)域事件模式
領(lǐng)域事件是一種通用模式,它的本質(zhì)是將領(lǐng)域概念添加到發(fā)布-訂閱模式。3.1 封裝領(lǐng)域事件的“發(fā)布-訂閱”模式
發(fā)布-訂閱是比較成熟的設(shè)計(jì)模式,具有很高的通用性。因此,建議針對(duì)領(lǐng)域需求進(jìn)行封裝。
比如直接使用 geekhalo-ffffd 相關(guān)模塊。
定義領(lǐng)域事件:
@Value public class LikeCancelledEvent extends AbstractAggregateEvent{ public LikeCancelledEvent(Like source) { super(source); } }
訂閱領(lǐng)域事件:
this.domainEventBus.register(LikeCancelledEvent.class, likeCancelledEvent->{ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); });
異步執(zhí)行領(lǐng)域事件:
DomainEventExecutor eventExecutor = new ExecutorBasedDomainEventExecutor("LikeEventHandler", 1, 100); this.domainEventBus.register(LikeCancelledEvent.class, eventExecutor, likeCancelledEvent->{ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); });3.2 內(nèi)存總線處理內(nèi)部事件,消息隊(duì)列處理外部事件
內(nèi)存總線簡(jiǎn)單高效,同時(shí)支持同步、異步兩個(gè)處理方案,比較適合處理繁雜的內(nèi)部事件;消息隊(duì)列雖然復(fù)雜,但擅長(zhǎng)解決服務(wù)間通信問題,適合處理外部事件。3.3 使用實(shí)體緩存領(lǐng)域事件
理論上,只有在業(yè)務(wù)成功完成后,才應(yīng)該對(duì)外發(fā)布事件。因此,將領(lǐng)域事件緩存在實(shí)體中,并在完成業(yè)務(wù)操作后將其進(jìn)行發(fā)布,是一種較好的解決方案。
相比,使用 ThreadLocal 管理訂閱器,并在事件 publish 時(shí)進(jìn)行訂閱回調(diào),事件緩存方案有明顯的優(yōu)勢(shì)。3.4 使用 IOC 容器的事件發(fā)布功能
IOC 容器為我們提供了很多使用功能,其中也包括發(fā)布-訂閱功能,如 Spring。
通常情況下,領(lǐng)域模型不應(yīng)該直接依賴于 Spring 容器。因此,在領(lǐng)域中我們?nèi)匀皇褂脙?nèi)存總線,為其添加一個(gè)訂閱者,將內(nèi)存總線中的事件轉(zhuǎn)發(fā)到 Spring 容器中。
class SpringEventDispatcher implements ApplicationEventPublisherAware { @Autowired private DomainEventBus domainEventBus; private ApplicationEventPublisher eventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } @PostConstruct public void addListener(){ this.domainEventBus.register(event->true, event -> {this.eventPublisher.publishEvent(event);}); } }
此時(shí),我們就可以直接使用 Spring 的 EventListener 機(jī)制對(duì)領(lǐng)域事件進(jìn)行處理。
@Component public class RedisBasedQueueExporter { @Autowired private RedisBasedQueue redisBasedQueue; @EventListener public void handle(LikeSubmittedEvent likeSubmittedEvent){ SubmittedEvent submittedEvent = new SubmittedEvent(); submittedEvent.setOwner(likeSubmittedEvent.getSource().getOwner()); submittedEvent.setTarget(likeSubmittedEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(submittedEvent); } @EventListener public void handle(LikeCancelledEvent likeCancelledEvent){ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); } }4 小結(jié)
領(lǐng)域事件是發(fā)生在問題域中的事實(shí),它是通用語(yǔ)言的一部分。
領(lǐng)域事件優(yōu)先使用發(fā)布-訂閱模式,會(huì)發(fā)布事件并且觸發(fā)相應(yīng)的事件處理器。
限界上下文內(nèi),優(yōu)先使用內(nèi)部事件和內(nèi)存總線;限界上下文間,優(yōu)先使用外部事件和消息隊(duì)列。
領(lǐng)域事件使異步操作變得簡(jiǎn)單。
領(lǐng)域事件為聚合間提供了最終一致性。
領(lǐng)域事件可以將大的批量操作簡(jiǎn)化為許多小的業(yè)務(wù)操作。
領(lǐng)域事件可以完成強(qiáng)大的事件存儲(chǔ)。
領(lǐng)域事件可以完成限界上下文間的集成。
領(lǐng)域事件是更復(fù)雜架構(gòu)(CQRS)的一種支持。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/74121.html
摘要:直到今天,提起云計(jì)算很多人仍喜歡和戰(zhàn)爭(zhēng)聯(lián)系在一起,這也不難理解,過去的多年中,試圖搶奪戰(zhàn)略優(yōu)勢(shì)的云服務(wù)商們已然發(fā)生了一系列的競(jìng)爭(zhēng),技術(shù)創(chuàng)新數(shù)據(jù)中心服務(wù)模式等無不是如此。也對(duì)應(yīng)了的另一個(gè)觀點(diǎn),云計(jì)算已經(jīng)不再是一個(gè)戰(zhàn)略問題,這是個(gè)戰(zhàn)術(shù)問題。圖片來源@視覺中國(guó)亞馬遜推出第一個(gè)云計(jì)算服務(wù)的時(shí)候,外界并沒有看好這個(gè)方向,高投入、低利潤(rùn)且存在很多的不確定性。但在2008年10月,《經(jīng)濟(jì)學(xué)人》破天荒的用一...
摘要:一微服務(wù)概念微服務(wù)體系結(jié)構(gòu)由輕量級(jí)松散耦合的服務(wù)集合組成。每個(gè)服務(wù)都有自己的計(jì)劃測(cè)試發(fā)布部署擴(kuò)展集成和獨(dú)立維護(hù)。團(tuán)隊(duì)不必因?yàn)檫^去的技術(shù)決定而受到懲罰。用在這里是指將相關(guān)的服務(wù)通過聚合器聚合在一起,這個(gè)聚合器就是門面。 微服務(wù)架構(gòu)現(xiàn)在是談到企業(yè)應(yīng)用架構(gòu)時(shí)必聊的話題,微服務(wù)之所以火熱也是因?yàn)橄鄬?duì)之前的應(yīng)用開發(fā)方式有很多優(yōu)點(diǎn),如更靈活、更能適應(yīng)現(xiàn)在需求快速變更的大環(huán)境。 一、微服務(wù)概念 微服...
摘要:為目前使用范圍最廣的網(wǎng)絡(luò)保護(hù)協(xié)議。身處攻擊目標(biāo)周邊的惡意人士能夠利用密鑰重裝攻擊,利用此類安全漏洞。本文和大家一起探討下如何在三年內(nèi)快速成長(zhǎng)為一名技術(shù)專家。 業(yè)界動(dòng)態(tài) Vue 2.5 released Vue 2.5 正式發(fā)布,作者對(duì)于該版本的優(yōu)化總結(jié):更好的TypeScript 集成,更好的錯(cuò)誤處理,更好的單文件功能組件支持以及更好的與環(huán)境無關(guān)的SSR WiFi爆驚天漏洞!KRACK...
摘要:年月日,平安科技在深圳平安金融中心舉辦了年平安科技優(yōu)秀培訓(xùn)合作伙伴交流會(huì),收到了邀請(qǐng)參與此次評(píng)選,并從余家合作伙伴中脫穎而出,在交付量滿意度師資內(nèi)容服務(wù)水準(zhǔn)等十余項(xiàng)指標(biāo)中獲得技術(shù)培訓(xùn)類年度優(yōu)秀合作伙伴獎(jiǎng)。 2018年12月4日,平安科技在深圳平安金融中心舉辦了2018年平安科技優(yōu)秀培訓(xùn)合作伙伴交流會(huì),msup收到了邀請(qǐng)參與此次評(píng)選,并從80余家合作伙伴中脫穎而出,在交付量、滿意度、師資...
閱讀 2350·2021-09-28 09:36
閱讀 2305·2021-09-22 15:14
閱讀 3711·2019-08-30 12:47
閱讀 3081·2019-08-30 12:44
閱讀 1306·2019-08-29 17:06
閱讀 598·2019-08-29 14:12
閱讀 1049·2019-08-29 14:01
閱讀 2635·2019-08-29 12:17