如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展
本系列所有文章
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(一)—— 先理解核心概念
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(二)—— 項(xiàng)目架構(gòu)
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(三)—— 初涉核心域
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(四)—— 把商品賣給用戶
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(五)—— 停下腳步,重新出發(fā)
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(六)—— 給購(gòu)物車加點(diǎn)料,集成售價(jià)上下文
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(七)—— 實(shí)現(xiàn)售價(jià)上下文
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(八)—— 會(huì)員價(jià)的集成
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(九)—— 小心陷入值對(duì)象持久化的坑
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(十)—— 一個(gè)完整的購(gòu)物車
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(十一)—— 最后的準(zhǔn)備
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(十二)—— 提交并生成訂單
如何一步一步用DDD設(shè)計(jì)一個(gè)電商網(wǎng)站(十三)—— 領(lǐng)域事件擴(kuò)展
?
?
閱讀目錄
- 前言
- 回顧
- 本地的一致性
- 領(lǐng)域事件發(fā)布出現(xiàn)異常
- 訂閱者處理出現(xiàn)異常
- 結(jié)語
?
一、前言
上篇中我們初步運(yùn)用了領(lǐng)域事件,其中還有一些問題我們沒有解決,所以實(shí)現(xiàn)是不健壯的,下面先來回顧一下。
?
二、回顧
先貼一下上篇中的遺留的問題:
public Result Create(OrderRequest orderRequest){if (!string.IsNullOrWhiteSpace(orderRequest.CouponId)){var couponResult = DomainRegistry.SellingPriceService().IsCouponCanUse(orderRequest.CouponId, orderRequest.OrderTime);if (!couponResult.IsSuccess)return Result.Fail(couponResult.Msg);}var orderId = DomainRegistry.OrderRepository().NextIdentity();var order = Domain.Order.Aggregate.Order.Create(orderId, orderRequest.UserId, orderRequest.Receiver,orderRequest.CountryId, orderRequest.CountryName, orderRequest.ProvinceId, orderRequest.ProvinceName,orderRequest.CityId, orderRequest.CityName, orderRequest.DistrictId, orderRequest.DistrictName,orderRequest.Address, orderRequest.Mobile, orderRequest.Phone, orderRequest.Email,orderRequest.PaymentMethodId, orderRequest.PaymentMethodName, orderRequest.ExpressId,orderRequest.ExpressName, orderRequest.Freight, orderRequest.CouponId, orderRequest.CouponName, orderRequest.CouponValue, orderRequest.OrderTime);foreach (var orderItemRequest in orderRequest.OrderItems){order.AddOrderItem(orderItemRequest.ProductId, orderItemRequest.Quantity, orderItemRequest.UnitPrice, orderItemRequest.JoinedMultiProductsPromotionId, orderItemRequest.ProductName);}DomainRegistry.OrderRepository().Save(order);DomainEventBus.Instance().Publish(new OrderCreated(order.ID, order.UserId, order.Receiver));return Result.Success();}不知道大家有沒有發(fā)現(xiàn)這里代碼上的一個(gè)問題,就是DomainEventBus.Instance().Publish()方法在聚合的Save操作之后進(jìn)行,其實(shí)本身不是很符合DDD的概念,任何的領(lǐng)域事件都是基于一個(gè)領(lǐng)域?qū)ο蟮?#xff0c;沒有領(lǐng)域?qū)ο蠛蝸眍I(lǐng)域事件,所以領(lǐng)域事件一般都是由領(lǐng)域?qū)ο髢?nèi)部產(chǎn)生,故這里應(yīng)該要把DomainEventBus.Instance().Publish()方法搬到Order.Create中調(diào)用。如果發(fā)現(xiàn)這個(gè)問題的童鞋,恭喜你對(duì)于領(lǐng)域事件的理解已經(jīng)又深入了一個(gè)層次了。好了上篇中這么寫其實(shí)是為了凸顯出本地?cái)?shù)據(jù)修改提交和領(lǐng)域事件的發(fā)布是涉及到數(shù)據(jù)一致性的問題的,其中的問題是:
1.如果領(lǐng)域事件發(fā)布出現(xiàn)異常了怎么辦?
2.如果訂閱者處理出現(xiàn)異常了怎么辦?
本篇我們就來一個(gè)一個(gè)解決問題。
?
三、本地的一致性
在解決上面的2個(gè)問題之前,我們先需要考慮在修改多個(gè)聚合的場(chǎng)景下本地上下文內(nèi)的一致性問題,這個(gè)職責(zé)在DDD中由工作單元(UnitOfWork)來負(fù)責(zé),工作單元就是為了保證本地的事務(wù)一致性,在.Net里的實(shí)現(xiàn)一般就是對(duì)SqlTransaction的封裝運(yùn)用。關(guān)于工作單元的實(shí)現(xiàn)一般有2種方式:
(1)完全依賴于SqlTransaction,在工作單元第一次運(yùn)用的時(shí)候就開啟數(shù)據(jù)庫事務(wù)。
(2)使用本地變量存儲(chǔ)變動(dòng)的聚合,然后在工作單元Commit()的時(shí)候開啟數(shù)據(jù)庫事務(wù)并寫入。
2個(gè)實(shí)現(xiàn)方案各有優(yōu)缺點(diǎn),需要在一致性和性能之間做出權(quán)衡。另外工作單元和領(lǐng)域事件發(fā)布的結(jié)合運(yùn)用可以參考我之前寫的2篇文章:DDD設(shè)計(jì)中的Unitwork與DomainEvent如何相容?和DDD中的Unitwork與DomainEvent如何相容?(續(xù)),注意的是我在這2篇中運(yùn)用的是方式(2)的實(shí)現(xiàn)方式。秉著沒有最好只有更好的精神,如何才能做到更好的一致性,這里需要引出幾個(gè)架構(gòu)層面的概念:ES、Saga、A+ES。這些內(nèi)容有一篇蟋蟀兄的文章(傳送門在此)講的很好,推薦大家閱讀一下,我就不展開講這些內(nèi)容了。里面每一種方案的運(yùn)用都有成本,大家根據(jù)實(shí)際情況權(quán)衡再運(yùn)用即可,切記:軟件開發(fā)中沒有銀彈。
?
四、領(lǐng)域事件發(fā)布出現(xiàn)異常
這個(gè)現(xiàn)象是否會(huì)出現(xiàn)需要根據(jù)領(lǐng)域事件發(fā)布的實(shí)現(xiàn)方式來決定,只要實(shí)現(xiàn)方式是“非本地”的方案,那么必然會(huì)出現(xiàn)一些異常的狀況。假如領(lǐng)域事件是通過消息隊(duì)列來實(shí)現(xiàn),那么涉及到了網(wǎng)絡(luò)傳輸必然會(huì)大大的增加出現(xiàn)異常的可能性。如何來解決此類問題,秉承著一圖勝千言的思想我直接貼個(gè)思維導(dǎo)圖,先看下一般的幾種實(shí)現(xiàn)方案的特點(diǎn),見圖1:
【圖1】
根據(jù)這個(gè)圖,我們發(fā)現(xiàn)魚和熊掌不可兼得,每個(gè)方案都由各自的特點(diǎn),我們應(yīng)當(dāng)根據(jù)不同的場(chǎng)景使用不同的實(shí)現(xiàn)方案去做,才是最好的選擇,并且據(jù)我所知,目前支持事務(wù)的消息隊(duì)列開源方案非常的少,所以我們需要通過一定的補(bǔ)償機(jī)制來處理與消息隊(duì)列通信出現(xiàn)問題的場(chǎng)景。另外在分布式系統(tǒng)中,服務(wù)端的接口設(shè)計(jì)盡量需要滿足無狀態(tài)和冪等性(不展開去講了,大家自行百度或者google),這也是整個(gè)系統(tǒng)高可用的重要的一環(huán)。最后的最后,通過對(duì)賬機(jī)制作為最后一道防線,確保重要的數(shù)據(jù)不產(chǎn)生差錯(cuò)。
那么我們來看一下這2個(gè)實(shí)現(xiàn)方案對(duì)應(yīng)我們的編碼應(yīng)該如何來做:
1.通過消息機(jī)制的發(fā)布就是把我在Demo中運(yùn)用DomainEventBus的內(nèi)部實(shí)現(xiàn)由Dictionary替換為外部的消息隊(duì)列即可,然后需要注冊(cè)DistributeExceptionEvent來處理丟給消息隊(duì)列進(jìn)行分發(fā)時(shí)出現(xiàn)異常的問題,做補(bǔ)償措施。
2.通過DB的方案,大致的偽代碼如下:
var unitOfWork = new UnitOfWork();unitOfWork.RegisterSaved(order);var domainEvents = GetEventsFromBus();foreach(var domainEvent in domainEvents){var body = Serialize(domainEvent);unitOfWork.RegisterSaved(new Message{Body = body});}return unitOfWork.Commit();大家可以看到,這個(gè)方式首先帶來的問題是讓工作單元變得異常的臃腫,隨之導(dǎo)致整個(gè)事務(wù)的總耗時(shí)增加。并且此時(shí)Message表中的現(xiàn)存數(shù)據(jù)可能還在同步進(jìn)行消費(fèi)/推送,那么產(chǎn)生資源競(jìng)爭(zhēng)是必然會(huì)遇到的問題,導(dǎo)致的后果是整個(gè)工作單元的提交失敗。
?
五、訂閱者處理出現(xiàn)異常
這個(gè)問題也是比較常見的,特別是處理業(yè)務(wù)復(fù)雜的接口和涉及過多RPC調(diào)用的接口出現(xiàn)的概率更大。所以每個(gè)應(yīng)用每個(gè)接口都需要考慮好此類問題。一般的解決方案我也梳理了一個(gè)思維導(dǎo)圖,如下圖2:
【圖2】
其實(shí)很明顯通過回滾的方式有很多局限性。所以說個(gè)人建議選擇下面的方案,盡量做到內(nèi)部消化,以提高接口對(duì)外的自治性。另外針對(duì)重試進(jìn)行一些限制,一是為了減少一些無用功來占用系統(tǒng)資源,二是避免在系統(tǒng)本身達(dá)到瓶頸的情況下出現(xiàn)馬太效應(yīng),讓擁堵問題越發(fā)嚴(yán)重。
?
六、結(jié)語
本篇沒有增加太多代碼,只是在Mall.Infrastructure中增加了幾個(gè)工作單元(方式(2))相關(guān)的類,其中只包含了一些核心邏輯代碼,具體的實(shí)現(xiàn)希望大家能夠自己動(dòng)手。多謝各位看官。
?
?
?
本文完整的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo13。
?
作者:Zachary_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/DDD_13.html
?
?
?關(guān)于作者:張帆(Zachary,個(gè)人微信號(hào):Zachary-ZF)。堅(jiān)持用心打磨每一篇高質(zhì)量原創(chuàng)。歡迎掃描右側(cè)的二維碼~。
定期發(fā)表原創(chuàng)內(nèi)容:架構(gòu)設(shè)計(jì)丨分布式系統(tǒng)丨產(chǎn)品丨運(yùn)營(yíng)丨一些思考。
?
如果你是初級(jí)程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關(guān)注我的公眾號(hào)「跨界架構(gòu)師」,回復(fù)「技術(shù)」,送你一份我長(zhǎng)期收集和整理的思維導(dǎo)圖。
如果你是運(yùn)營(yíng),面對(duì)不斷變化的市場(chǎng)束手無策。又或者想了解主流的運(yùn)營(yíng)策略,以豐富自己的“倉庫”。歡迎關(guān)注我的公眾號(hào)「跨界架構(gòu)師」,回復(fù)「運(yùn)營(yíng)」,送你一份我長(zhǎng)期收集和整理的思維導(dǎo)圖。
轉(zhuǎn)載于:https://www.cnblogs.com/Zachary-Fan/p/DDD_13.html
總結(jié)
以上是生活随笔為你收集整理的如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [svc][op]从历险压缩日志里网站p
- 下一篇: [dpdk] SDK编译-简单扼要版