閱讀目錄 前言 回顧 本地的一致性 領域事件發佈出現異常 訂閱者處理出現異常 結語 一、前言 上篇中我們初步運用了領域事件,其中還有一些問題我們沒有解決,所以實現是不健壯的,下麵先來回顧一下。 二、回顧 先貼一下上篇中的遺留的問題: 不知道大家有沒有發現這裡代碼上的一個問題,就是DomainEven ...
閱讀目錄
一、前言
上篇中我們初步運用了領域事件,其中還有一些問題我們沒有解決,所以實現是不健壯的,下麵先來回顧一下。
二、回顧
先貼一下上篇中的遺留的問題:
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:
【圖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:
【圖2】
其實很明顯通過回滾的方式有很多局限性。所以說個人建議選擇下麵的方案,儘量做到內部消化,以提高介面對外的自治性。另外針對重試進行一些限制,一是為了減少一些無用功來占用系統資源,二是避免在系統本身達到瓶頸的情況下出現馬太效應,讓擁堵問題越發嚴重。
六、結語
本篇沒有增加太多代碼,只是在Mall.Infrastructure中增加了幾個工作單元(方式(2))相關的類,其中只包含了一些核心邏輯代碼,具體的實現希望大家能夠自己動手。多謝各位看官。
本文完整的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo13。
作者:Zachary_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/DDD_13.html