DDD領(lǐng)域驅(qū)動(dòng)/CQRS讀寫分離/ES事件溯源 這些前沿的時(shí)髦的技術(shù)理念匯聚在一次赡茸,落地到一套完整實(shí)現(xiàn)方案。這就是Axon
我們從ES事件溯源開始說
- 傳統(tǒng)的數(shù)據(jù)庫設(shè)計(jì)只記錄數(shù)據(jù)的當(dāng)前狀態(tài)深滚,對于輸入如何到達(dá)當(dāng)前狀態(tài)的過程信息則并不重視。
- 而ES采用了完全不同的記錄方式涣觉,它記錄所有改變當(dāng)前狀態(tài)的事件痴荐。并從這些事件累加出當(dāng)前狀態(tài)。它于傳統(tǒng)方式相比有如下特性:
- 更好的追溯問題的起因官册。(傳統(tǒng)方式只能到log中找)
- 更大的數(shù)據(jù)存儲(是缺點(diǎn)也是優(yōu)點(diǎn)生兆,有利于數(shù)據(jù)挖掘)
- 對于事件只插入不更新。沒有鎖膝宁,吞吐量更高
- 更復(fù)雜的實(shí)現(xiàn)
- ES還有一種輔助機(jī)制來減少溯源的計(jì)算量鸦难,定期做一個(gè)當(dāng)前狀態(tài)的快照。事件在最近的快照后進(jìn)行累加
領(lǐng)域驅(qū)動(dòng)中提到過充血/失血模型
- 傳統(tǒng)實(shí)現(xiàn)是失血模型员淫,數(shù)據(jù)和行為是分離的合蔽。數(shù)據(jù)存儲在數(shù)據(jù)庫中,使用時(shí)加載到應(yīng)用服務(wù)器介返,然后處理之后再存入數(shù)據(jù)庫拴事。
- 而真正面相對象的設(shè)計(jì)中,數(shù)據(jù)和行為是在一起的(比如Actor模型)圣蝎,即服務(wù)層即執(zhí)行邏輯也持有數(shù)據(jù)刃宵。
- 當(dāng)然傳統(tǒng)的失血模型有它的好處。即服務(wù)是無狀態(tài)的徘公,可以方便的做負(fù)載擴(kuò)展牲证。Axon的充血模型是怎么處理這個(gè)問題的,之后再深入步淹。
- CQRS讀寫分離从隆。如果實(shí)施了事件溯源诚撵,那么數(shù)據(jù)庫里就只保存事件了,正常的業(yè)務(wù)查詢是不能直接使用這些數(shù)據(jù)的键闺。這個(gè)時(shí)候就需要視圖表來展示數(shù)據(jù)寿烟,視圖表都是從事件表轉(zhuǎn)換而來的。
當(dāng)然DDD領(lǐng)域驅(qū)動(dòng)/CQRS讀寫分離/ES事件溯源這些不是強(qiáng)制綁定的辛燥,可以隨意一選擇筛武,下面是Axon的架構(gòu)圖:
下面我們用axon來實(shí)踐開發(fā)一個(gè)游戲(代碼使用的是Axon4.0)
首選我們創(chuàng)建一個(gè)領(lǐng)域?qū)ο蟆婕襊layer
@Aggregate
public class Player{
@AggregateIdentifier
private String id;
private String name;//名字
private PlayerStatus status;//狀態(tài)
@CommandHandler
public Player(CreatePlayerCmd cmd) {
AggregateLifecycle.apply(new PlayerCreatedEvent(
IdentifierFactory.getInstance().generateIdentifier()
, cmd.getPlayerName(), new Date()));
}
@EventSourcingHandler
private void on(PlayerCreatedEvent event) {
this.id = event.getId();
this.name = event.getPlayerName();
this.status = PlayerStatus.ACTIVE;
}
}
- @Aggregate會被Spring加載并自動(dòng)賦予axon相關(guān)的功能
- @AggregateIdentifier 指定聚合根的ID,必須要有
- @CommandHandler 用來處理外部觸發(fā)的行為和計(jì)算
- @EventSourcingHandler 用作狀態(tài)更改和事件回放(通過事件重放回復(fù)當(dāng)前狀態(tài))
接著我們實(shí)現(xiàn)一個(gè)隨著時(shí)間自然消耗的功能
EventSourcingHandler
private Date lastSustainLivingDate;//上次維持生存的計(jì)算時(shí)間節(jié)點(diǎn)
@CommandHandler
public void handle(SustainLivingCmd cmd){
AggregateLifecycle.apply(new SustainLivingEvent(cmd.getTiggerDate(), lastSustainLivingDate));
}
@EventSourcingHandler
protected void on(SustainLivingEvent event) {
this.lastSustainLivingDate = event.getTriggerDate();
}
- 寫到這里發(fā)現(xiàn)每實(shí)現(xiàn)一個(gè)功能是比較繁瑣的挎塌。要?jiǎng)?chuàng)建兩個(gè)方法徘六,一個(gè)命令對象,一個(gè)事件對象榴都。中間是一些繁瑣的值傳遞待锈。
- AggregateLifecycle.apply()做了幾件事情
- 調(diào)用實(shí)例自己的@EventSourcingHandler
- 通過EventBus存儲和傳播事件
- 外部事件監(jiān)聽器監(jiān)聽,主要是CQRS中的查詢視圖嘴高,更新試圖數(shù)據(jù)竿音。
PlayerEntry保存了所有玩家的列表幷提供查詢,它監(jiān)聽PlayerCreatedEvent
@Component
public class PlayerEntryListener {
@Autowired
private PlayerEntryRepository playerEntryRepository;
@EventHandler
public void on(PlayerCreatedEvent event) {
PlayerEntry playerEntry = new PlayerEntry();
playerEntry.setId(event.getId());
playerEntry.setName(event.getPlayerName());
playerEntryRepository.save(playerEntry);
}
}
Command執(zhí)行過程:
- Command通過org.axonframework.commandhandling.CommandBus分發(fā)拴驮,其中DistributedCommandBus實(shí)現(xiàn)了分布式的派發(fā)春瞬,它可以適配SpringCloud. 主要邏輯是通過
AggregateIdentifier做一致性HASH。確保次對同一個(gè)聚合根發(fā)到同一臺機(jī)器套啤,這樣保證了緩存的命中宽气。Aggregate本質(zhì)上在應(yīng)用內(nèi)緩存了當(dāng)前狀態(tài),如果該服務(wù)宕機(jī)潜沦。另一臺機(jī)器會通過事件溯源重新構(gòu)造出當(dāng)前狀態(tài)萄涯。
- Command的事務(wù)處理:Axon通過UnitofWork控制一致性,其中嵌入了JDBC事務(wù)止潮。并且在回滾時(shí)會清除Aggregate緩存窃判,下次會重新加載。
- 當(dāng)然對于遠(yuǎn)程的子事務(wù)無法保證一致性喇闸。這是個(gè)大的隱患
Saga最終一致性袄琳,補(bǔ)償機(jī)制。
- Saga的理念沒問題燃乍,當(dāng)是實(shí)現(xiàn)起來問題很大唆樊。
- 一個(gè)是每個(gè)command都要?jiǎng)?chuàng)建event非常繁瑣,并且saga來來回回非常多
- 另一個(gè)是沒有考慮到冪等刻蟹,本地宕機(jī)逗旁,狀態(tài),分布事務(wù)等細(xì)節(jié)的處理。自己實(shí)現(xiàn)也不靈活
- 確保分布式事務(wù)一致性是個(gè)非常繁瑣的事情片效,請參考轉(zhuǎn)賬交易這件小事红伦,是如何被程序員玩壞的.
說說其他實(shí)現(xiàn)困難的情況
- 如果Aggregate存在繼承關(guān)系,或者實(shí)現(xiàn)一些通用的行為接口淀衣£级粒框架并不支持
- 無法實(shí)現(xiàn)延遲觸發(fā)的功能
- 對大量聚合實(shí)現(xiàn)批量操作不太容易
- 幾乎所有的操作都要靠command觸發(fā),然后轉(zhuǎn)成event,非常繁瑣膨桥。
取其思想蛮浑,尋找替代
在交易中的訂單模型,本質(zhì)上就是ES中的Event. 賬戶余額是用訂單累加出來的只嚣。只不過訂單不止記錄事件沮稚。還用來記錄更新審核狀態(tài),系統(tǒng)狀態(tài)册舞,事務(wù)狀態(tài)等蕴掏,不那么純粹。通過訂單回溯賬戶狀態(tài)是個(gè)人工過程而不是自動(dòng)的环础。
- 在這方面囚似,Axon對事件的存儲采用統(tǒng)一的表,事件序列化到表中线得,并不有利于數(shù)據(jù)庫的查看。需要額外的記錄一個(gè)查詢視圖
- 快照模型也在交易對賬中有所體現(xiàn)徐伐。每日對賬后生成當(dāng)日的一個(gè)快照贯钩,第二天的交易從快照開始累計(jì)
Command模型配合只insert沒有update的操作,可以在高并發(fā)下實(shí)現(xiàn)無鎖處理办素。這個(gè)可以通過Kafka角雷。或者數(shù)據(jù)庫先插入性穿,后異步讀取處理的方式來實(shí)現(xiàn)勺三。
充血模型是Axon的一個(gè)特色。不過并不難實(shí)現(xiàn)需曾。通過自定義一個(gè)SpringCloud LoadBalancer Rule 實(shí)現(xiàn)哈希一致也可以實(shí)現(xiàn)吗坚。另外傳統(tǒng)的失血模型使用無狀態(tài)服務(wù)更簡單,它可以在數(shù)據(jù)庫層面通過一致性hash來存儲數(shù)據(jù)呆万,比如mongodb商源。對于訂單這樣的單一記錄低頻操作完全可以處理。對于同一記錄的高頻處理Axon也是有其優(yōu)勢的谋减,不過Akka可能也是不錯(cuò)的選擇
Saga異常難寫牡彻,在網(wǎng)絡(luò)異常,冪等出爹,重試方面并不友好
總結(jié):DDD領(lǐng)域驅(qū)動(dòng)/CQRS讀寫分離/ES事件溯源庄吼,這些思想都很棒缎除,但是并不用拘泥于特定的實(shí)現(xiàn)。Axon對于學(xué)習(xí)這些概念具有非常大的幫助总寻,但是在實(shí)踐的過程中幷不高效器罐、靈活