Event?Sourcing事件溯源模式優(yōu)化業(yè)務(wù)系統(tǒng)
正文
高內(nèi)聚低耦合一直是程序設(shè)計(jì)提倡的方式,但是很多人在實(shí)際項(xiàng)目中一直再使用面向過(guò)程的編碼,導(dǎo)致代碼臃腫不堪,項(xiàng)目維護(hù)難度日益增加,在我接觸的初中高級(jí)程序員中,很多人曾問(wèn)我如何從設(shè)計(jì)階段初期盡量避免日后帶來(lái)維護(hù)難度,今天就從Event Soucing(事件溯源)模式聊聊如何優(yōu)化我們的業(yè)務(wù)系統(tǒng)。
枯燥的理論知識(shí)避不可免,下面我盡量以代碼形式去演示事件驅(qū)動(dòng)給在我們業(yè)務(wù)編程中帶來(lái)的好處。
什么是Event Sourcing ?
簡(jiǎn)單來(lái)說(shuō),大家應(yīng)該都知道m(xù)ysql 數(shù)據(jù)同步中的binlog模式,我們?cè)趫?zhí)行一條查詢語(yǔ)句 select * from Person limit 1 時(shí)看到的數(shù)據(jù)可以理解為當(dāng)前時(shí)間的快照,眼前的這條數(shù)據(jù),可能經(jīng)歷過(guò)若干update語(yǔ)句之后才得到的結(jié)果,事件溯源亦如此,如果我們把某一行數(shù)據(jù) 看做Person對(duì)象,一個(gè)對(duì)象從開(kāi)始到消亡會(huì)經(jīng)歷過(guò)很多事件(update語(yǔ)句),如果我們要還原某個(gè)時(shí)間點(diǎn)的對(duì)象,只需依據(jù)的產(chǎn)生日期,按照順序在初始化對(duì)象上依次疊加上去,就能還原這一時(shí)期的對(duì)象了,
舉個(gè)例子一個(gè)person(張三)對(duì)象
Person zs = new Person(); 張三出生了
6歲 ?? 學(xué)生
25歲 ?? 警察
60歲 ?? 退休老人
雖然都是張三對(duì)象,但是不同時(shí)間段里張三的身份截然不同,如果我們要獲取警察時(shí)代的zs,我們用初始得到的zs依次累加上學(xué)生時(shí)代,警察時(shí)代就可以得到這一時(shí)代的zs對(duì)象了。
由此來(lái)看,對(duì)象好像顯得已經(jīng)不那么重要,事件溯源更加具有意義,因?yàn)樗暾枋隽诉@個(gè)對(duì)象從出生到消亡的全過(guò)程,也可以看為不斷在改變對(duì)象的狀態(tài),事件是只會(huì)增加不會(huì)修改,對(duì)于現(xiàn)如今大數(shù)據(jù)時(shí)代,事件的產(chǎn)生對(duì)于數(shù)據(jù)挖掘、數(shù)據(jù)分析更具有意義。
扯了這么多,還是要以代碼來(lái)實(shí)際說(shuō)說(shuō)事件驅(qū)動(dòng)帶來(lái)的好處,先看一處經(jīng)典的代碼
StockService.java
@Service @AllArgsConstructor public class StockService extends BaseMapper<Product> { //京東服務(wù) private final JdService jdService; //淘寶服務(wù) private final TaobaoService productService; //有贊服務(wù) private final YouzanService youzanService; //拼多多服務(wù) private final pddService pddService; //更多服務(wù) ... //設(shè)置商品庫(kù)存 @Override public void changeProductStock(ChangeProductStockInputDTO inputDTO) { if(inputDTO.getStock<0){ throw new BusinessException("庫(kù)存不能小于0"); } Product product = baseMapper.getById(inputDTO.getId()); product.setStock(inputDTO.getStock()); baseMapper.updateById(product); //通知京東 jdService.notify(); //通知淘寶 productService.notify(); //通知有贊 youzanService.notify(); //更多需要執(zhí)行的業(yè)務(wù)... } }
Product.java
@Data public class Product { //id private String id; //庫(kù)存 private BigDecimal stock; //... }
例如比如在電商系統(tǒng)中,在我們自己的商品后臺(tái)中修改商品庫(kù)存后,我們要依次告知在其他第三方平臺(tái)這個(gè)商品庫(kù)存信息,我相信很多同學(xué)都會(huì)這樣寫(xiě)的吧,這樣的代碼確實(shí)可以完成我們的業(yè)務(wù)功能,但隨著業(yè)務(wù)功能的復(fù)雜度提升,加上我們面向過(guò)程的編碼模式,一定會(huì)越加復(fù)雜,曾看到有將近5000多行的一個(gè)訂單類,相信不管誰(shuí)看見(jiàn)這樣的類都會(huì)頭大,接下來(lái)我們就要想辦法優(yōu)化它,安排!
首先存在這樣的代碼是因?yàn)闆](méi)有劃清邊界,沒(méi)有保持一個(gè)領(lǐng)域中的純粹性,從StockService中注入大量的服務(wù)類與標(biāo)志性的貧血模型Product對(duì)象就能看出,既然我們提倡以高內(nèi)聚低耦合去編寫(xiě)代碼,那首先去修改我們的Product吧,讓它變得豐富起來(lái)。
改變的Product.java
@Data public class Product { public void changeStock(BigDecimal stock){ if(delStatus == 1){ throw new BusinessException("商品信息不存在"); } if(stock < 0){ throw new BusinessException("庫(kù)存不能小于0"); } this.stock = stock; EventBus.instance().register(new ChangedProductStockDomainEvent(this)); } //id private String id; //庫(kù)存 private BigDecimal stock; //刪除狀態(tài) private int delStatus; //... } //名字盡量起得生動(dòng)一些,單詞語(yǔ)法的過(guò)去式,現(xiàn)在進(jìn)行時(shí)都具有意義 @Getter @AllArgsConstructor public class ChangedProductStockDomainEvent { private Product product; }
更改的 StockService.java
@Service @AllArgsConstructor public class StockService extends BaseMapper<Product> { //設(shè)置商品庫(kù)存 @Override public void changeProductStock(ChangeProductStockInputDTO inputDTO) { Product product = baseMapper.getById(inputDTO.getId()); product.setProductStock(inputDTO.getProductStock()); } }
更改過(guò)后的代碼是不是看起來(lái)清爽了很多,加上我們賦予了Product對(duì)象方法之后,職責(zé)看起來(lái)就更加明確,充血模型體現(xiàn)出聚合內(nèi)單一的行為,在Product中我們只描述了此領(lǐng)域范圍的職能,已經(jīng)充分體現(xiàn)了高內(nèi)聚低耦合的思想,不參合其他業(yè)務(wù)邏輯。這時(shí)可能有的同學(xué)會(huì)問(wèn)那怎么持久化到數(shù)據(jù)庫(kù)呢?在我工作的這些年里,遇到很多程序員,不論初中高級(jí)程序員都習(xí)慣了先建立數(shù)據(jù)庫(kù),再去建立模型,但是我們要改變傳統(tǒng)思維,我們寫(xiě)代碼是面向?qū)ο?,面向?qū)ο?,面向?qū)ο螅ㄖ匾氖虑檎f(shuō)三遍),不是面向數(shù)據(jù)或者過(guò)程,在剝離了數(shù)據(jù)后,其實(shí)我們真正就做到了數(shù)據(jù)與業(yè)務(wù)代碼的剝離,下面我在說(shuō)這樣具體的好處。
細(xì)心的同學(xué)看到我在Product的changeStock方法里,在執(zhí)行完一些邏輯判斷后,設(shè)置完商品庫(kù)存后,我們?cè)贓ventBus 事件總線中注冊(cè)了一個(gè)事件,這個(gè)事件還沒(méi)有具體的作用,我們看看EventBus的實(shí)現(xiàn)
StockService.java
public class EventBus { public static EventBus instance() { return new EventBus(); } private static final ThreadLocal<List<DomainEvent>> domainEvents = new ThreadLocal<>(); public void init() { if (domainEvents.get() == null) { domainEvents.set(new ArrayList<>()); } } public EventBus register(DomainEvent domainEvent) { List<DomainEvent> domainEventList = domainEvents.get(); if (domainEventList == null) throw new IllegalArgumentException("domainEventList not init"); domainEventList.add(domainEvent); return this; } /** * 獲取領(lǐng)域事件 * * @return */ public List<DomainEvent> getDomainEvent() { return domainEvents.get(); } /** * 請(qǐng)空領(lǐng)域事件集合 */ public void reset() { domainEvents.set(null); } }
在當(dāng)前線程內(nèi)內(nèi)存空間我們吧事件塞了進(jìn)去,目前只有存儲(chǔ)作用,接下來(lái)我們要定義它的處理者
DomainEventProcessor.java
@Aspect @Component @Slf4j public class DomainEventProcessor { /** * 這里我是我對(duì)RocketMq的封裝 */ @Autowired private EventPublisherExecutor processPublisherExecutor; /** * 當(dāng)前上下文內(nèi)訂閱者 */ @Autowired protected ApplicationContext applicationContext; private static ThreadLocal<AtomicInteger> counter = new ThreadLocal<>(); @Pointcut("within(com.github.tom.*.application..*)") public void aopRule() { } /** * 為當(dāng)前線程初始化EventBus */ @Before("aopRule()") public void initEventBus(JoinPoint joinPoint) { log.debug("初始化領(lǐng)域事件槽"); log.debug("切入切點(diǎn)信息:" + joinPoint.getSignature().toString()); EventBus.instance().init(); if (counter.get() == null) { counter.set(new AtomicInteger(0)); } counter.get().incrementAndGet(); } /** * 發(fā)布領(lǐng)域事件 */ @AfterReturning("aopRule()") public void publish() { int count = counter.get().decrementAndGet(); if (count == 0) { try { List<DomainEvent> domainEventList = EventBus.instance().getDomainEvent(); if (domainEventList != null && domainEventList.size() > 0) { //進(jìn)程內(nèi)事件 domainEventList.forEach(domainEvent -> applicationContext.publishEvent(domainEvent)); //進(jìn)程外事件 domainEventList.forEach(domainEvent -> processPublisherExecutor.publish(domainEvent)); } } finally { EventBus.instance().reset(); counter.set(null); } } } @AfterThrowing(throwing = "ex", pointcut = "aopRule()") public void exception(Throwable ex) { log.error(ex.getMessage(), ex); EventBus.instance().reset(); //釋放計(jì)數(shù)器 counter.set(null); } }
這里借助了AOP功能,在AOP內(nèi)我對(duì)service進(jìn)行攔截,在執(zhí)行方法攔截的出口時(shí),查找當(dāng)前線程內(nèi)的EventBus中看是否有存在的領(lǐng)域事件,接下來(lái)把事件發(fā)送出去,事件的響應(yīng)分為進(jìn)程內(nèi)和進(jìn)程外(多微服務(wù)),剛才的同學(xué)問(wèn)的如何持久化到DB這里可以看到答案
@Slf4j public abstract class AbstractEventHandler<T extends EventData> implements SmartApplicationListener { private Class<?> clazzType; public AbstractEventHandler(Class<? extends ApplicationEvent> clazzType) { this.clazzType = clazzType; } @Override public boolean supportsEventType(Class<? extends ApplicationEvent> clazzType) { return clazzType == this.clazzType; } @Override public boolean supportsSourceType(Class<?> clazz) { return true; } @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { onApplicationEventHandler((T) applicationEvent); } protected abstract void onApplicationEventHandler(T eventData); } @Slf4j public abstract class AbstractPersistenceEventHandler<T extends EventData> extends AbstractEventHandler<T> { public AbstractPersistenceEventHandler(Class<? extends ApplicationEvent> clazzType) { super(clazzType); } @Override public int getOrder() { return 0; } } @Component public class ChangeProductStockPersistenceEventHandler extends AbstractPersistenceEventHandler<ChangedProductStockDomainEvent> { @Autowired private ProductRepository productRepository; public CreatedPortalArticlePersistenceEventHandler() { super(CreatedPortalArticleDomainEvent.class); } @Override protected void onApplicationEventHandler(ChangedProductStockDomainEvent eventData) { if (portalArticleRepository.updateById(eventData.getProduct()) <= 0) { throw new BusinessException("數(shù)據(jù)操作錯(cuò)誤"); } } }
在響應(yīng)事件的其中一個(gè)訂閱者,可以完成數(shù)據(jù)庫(kù)的持久化操作。接下來(lái)我們?nèi)ザx各個(gè)響應(yīng)ChangedProductStockDomainEvent事件的訂閱者就行,例如京東服務(wù)
@Component public class JdStockEventHandler { @Autowired private JdAppService jdAppService; /** * 庫(kù)存持久化事件 * * @param eventData */ @StreamListener(value = "product-channel") public void receive(@Payload ChangedProductStockDomainEvent eventData) { jdAppService.changingInventory(eventData); } }
事件驅(qū)動(dòng)的模型大大降低了業(yè)務(wù)模塊耦合嚴(yán)重,在每個(gè)聚合的領(lǐng)域內(nèi),我們應(yīng)該著重自身聚合的業(yè)務(wù)邏輯,事件的消費(fèi)我們可以通過(guò)廣播通知和最終一致性來(lái)達(dá)成目的。業(yè)務(wù)代碼的純粹,也更適合TDD只對(duì)業(yè)務(wù)編寫(xiě)測(cè)試代碼,例如我在編寫(xiě)設(shè)置庫(kù)存的測(cè)試方法時(shí),我只要構(gòu)造好商品對(duì)象,就可以按照測(cè)試用例編寫(xiě)不同情況下的測(cè)試代碼了。
@Component public class ProductStockTest { @Before public void setUp() { EventBus.instance().init(); } @Test public void testChangeStockError() { Product product = new Product(); product.setStock(BigDecimal.valueOf("-1")); product.changeStock(); } @Test public void testChangeStockSuccess() { Product product = new Product(); product.setStock(BigDecimal.valueOf("2")); product.changeStock(); assertThat(product.getStock()).isEqualTo("2"); } }
好了今天的介紹就先這么多,后面我會(huì)介紹如何讓三層架構(gòu)中的Service層升級(jí),變得充滿業(yè)務(wù)味道(領(lǐng)域服務(wù))。
以上就是Event Sourcing事件溯源模式優(yōu)化業(yè)務(wù)系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于Event Sourcing事件溯源的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot使用ApplicationEvent&Listener完成業(yè)務(wù)解耦
- 基于Spring Boot應(yīng)用ApplicationEvent案例場(chǎng)景
- 詳解SpringBoot實(shí)現(xiàn)ApplicationEvent事件的監(jiān)聽(tīng)與發(fā)布
- SpringBoot中ApplicationEvent和ApplicationListener用法小結(jié)
- SpringBoot Event 事件如何實(shí)現(xiàn)異步延遲執(zhí)行
- spring event 事件異步處理方式(發(fā)布,監(jiān)聽(tīng),異步處理)
相關(guān)文章
SpringBoot之spring.factories的使用方式
這篇文章主要介紹了SpringBoot之spring.factories的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01Java Set集合及其子類HashSet與LinkedHashSet詳解
這篇文章主要介紹了Java Set集合及其子類HashSet與LinkedHashSet詳解,文章通過(guò)Set集合存儲(chǔ)原理展開(kāi)文章主題相關(guān)介紹,感興趣的小伙伴可以參考一下2022-06-06java反射機(jī)制給實(shí)體類相同字段自動(dòng)賦值實(shí)例
這篇文章主要介紹了java反射機(jī)制給實(shí)體類相同字段自動(dòng)賦值實(shí)例,具有2020-08-08springboot?publish?event?事件機(jī)制demo分享
這篇文章主要介紹了springboot?publish?event?事件機(jī)制demo,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10SpringBoot?整合?Quartz?定時(shí)任務(wù)框架詳解
這篇文章主要介紹了SpringBoot整合Quartz定時(shí)任務(wù)框架詳解,Quartz是一個(gè)完全由Java編寫(xiě)的開(kāi)源作業(yè)調(diào)度框架,為在Java應(yīng)用程序中進(jìn)行作業(yè)調(diào)度提供了簡(jiǎn)單卻強(qiáng)大的機(jī)制2022-08-08spring多數(shù)據(jù)源配置實(shí)現(xiàn)方法實(shí)例分析
這篇文章主要介紹了spring多數(shù)據(jù)源配置實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了spring多數(shù)據(jù)源配置相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2019-12-12ssm 使用token校驗(yàn)登錄的實(shí)現(xiàn)
這篇文章主要介紹了ssm 使用token校驗(yàn)登錄的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04解決JavaWeb讀取本地json文件以及亂碼的問(wèn)題
今天小編就為大家分享一篇解決JavaWeb讀取本地json文件以及亂碼的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-06-06SpringBoot 利用MultipartFile上傳本地圖片生成圖片鏈接的實(shí)現(xiàn)方法
這篇文章主要介紹了SpringBoot 利用MultipartFile上傳本地圖片生成圖片鏈接的實(shí)現(xiàn)方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03