如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展


本系列所有文章

如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念

如何一步一步用DDD設計一個電商網站(二)—— 項目架構

如何一步一步用DDD設計一個電商網站(三)—— 初涉核心域

如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶

如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發

如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文

如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文

如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成

如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑

如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車

如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備

如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單

如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展

 

 

閱讀目錄

 

一、前言

  上篇中我們初步運用了領域事件,其中還有一些問題我們沒有解決,所以實現是不健壯的,下面先來回顧一下。

 

二、回顧

  先貼一下上篇中的遺留的問題:

        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();
        }

  不知道大家有沒有發現這里代碼上的一個問題,就是DomainEventBus.Instance().Publish()方法在聚合的Save操作之后進行,其實本身不是很符合DDD的概念,任何的領域事件都是基於一個領域對象的,沒有領域對象何來領域事件,所以領域事件一般都是由領域對象內部產生,故這里應該要把DomainEventBus.Instance().Publish()方法搬到Order.Create中調用。如果發現這個問題的童鞋,恭喜你對於領域事件的理解已經又深入了一個層次了。好了上篇中這么寫其實是為了凸顯出本地數據修改提交和領域事件的發布是涉及到數據一致性的問題的,其中的問題是:

  1.如果領域事件發布出現異常了怎么辦?

  2.如果訂閱者處理出現異常了怎么辦?

  本篇我們就來一個一個解決問題。

 

三、本地的一致性

  在解決上面的2個問題之前,我們先需要考慮在修改多個聚合的場景下本地上下文內的一致性問題,這個職責在DDD中由工作單元(UnitOfWork)來負責,工作單元就是為了保證本地的事務一致性,在.Net里的實現一般就是對SqlTransaction的封裝運用。關於工作單元的實現一般有2種方式:

  (1)完全依賴於SqlTransaction,在工作單元第一次運用的時候就開啟數據庫事務。

  (2)使用本地變量存儲變動的聚合,然后在工作單元Commit()的時候開啟數據庫事務並寫入。

  2個實現方案各有優缺點,需要在一致性和性能之間做出權衡。另外工作單元和領域事件發布的結合運用可以參考我之前寫的2篇文章:DDD設計中的Unitwork與DomainEvent如何相容?DDD中的Unitwork與DomainEvent如何相容?(續),注意的是我在這2篇中運用的是方式(2)的實現方式。秉着沒有最好只有更好的精神,如何才能做到更好的一致性,這里需要引出幾個架構層面的概念:ES、Saga、A+ES。這些內容有一篇蟋蟀兄的文章(傳送門在此)講的很好,推薦大家閱讀一下,我就不展開講這些內容了。里面每一種方案的運用都有成本,大家根據實際情況權衡再運用即可,切記:軟件開發中沒有銀彈。

 

四、領域事件發布出現異常

  這個現象是否會出現需要根據領域事件發布的實現方式來決定,只要實現方式是“非本地”的方案,那么必然會出現一些異常的狀況。假如領域事件是通過消息隊列來實現,那么涉及到了網絡傳輸必然會大大的增加出現異常的可能性。如何來解決此類問題,秉承着一圖勝千言的思想我直接貼個思維導圖,先看下一般的幾種實現方案的特點,見圖1:

DDD13(1)

                             【圖1】

  根據這個圖,我們發現魚和熊掌不可兼得,每個方案都由各自的特點,我們應當根據不同的場景使用不同的實現方案去做,才是最好的選擇,並且據我所知,目前支持事務的消息隊列開源方案非常的少,所以我們需要通過一定的補償機制來處理與消息隊列通信出現問題的場景。另外在分布式系統中,服務端的接口設計盡量需要滿足無狀態和冪等性(不展開去講了,大家自行百度或者google),這也是整個系統高可用的重要的一環。最后的最后,通過對賬機制作為最后一道防線,確保重要的數據不產生差錯。

  那么我們來看一下這2個實現方案對應我們的編碼應該如何來做:

  1.通過消息機制的發布就是把我在Demo中運用DomainEventBus的內部實現由Dictionary替換為外部的消息隊列即可,然后需要注冊DistributeExceptionEvent來處理丟給消息隊列進行分發時出現異常的問題,做補償措施。

  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();

  大家可以看到,這個方式首先帶來的問題是讓工作單元變得異常的臃腫,隨之導致整個事務的總耗時增加。並且此時Message表中的現存數據可能還在同步進行消費/推送,那么產生資源競爭是必然會遇到的問題,導致的后果是整個工作單元的提交失敗。

 

五、訂閱者處理出現異常

  這個問題也是比較常見的,特別是處理業務復雜的接口和涉及過多RPC調用的接口出現的概率更大。所以每個應用每個接口都需要考慮好此類問題。一般的解決方案我也梳理了一個思維導圖,如下圖2:

DDD13(2)

                              【圖2】

  其實很明顯通過回滾的方式有很多局限性。所以說個人建議選擇下面的方案,盡量做到內部消化,以提高接口對外的自治性。另外針對重試進行一些限制,一是為了減少一些無用功來占用系統資源,二是避免在系統本身達到瓶頸的情況下出現馬太效應,讓擁堵問題越發嚴重。

 

六、結語

  本篇沒有增加太多代碼,只是在Mall.Infrastructure中增加了幾個工作單元(方式(2))相關的類,其中只包含了一些核心邏輯代碼,具體的實現希望大家能夠自己動手。多謝各位看官。

 

 

 

本文完整的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo13

 

作者:Zachary
出處:https://zacharyfan.com/archives/199.html

 

 

▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。

定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。

 

如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。

如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM