阅读目录
一、前言
之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍。现在来到了这最后一个环节,提交订单。单从业务上看,这个动作的背后包含了多个业务操作,根据用户填写的订单信息生成订单、扣除使用的余额和积分、使用选择的礼券等等。其中涉及到多个上下文的操作,包括新引入的订单上下文,那么如何同时与多个上下文进行数据的写入操作是本篇主要想讨论的问题。
二、解决数据一致性的方案
分布式系统中的多个子系统之间的同时写入问题,也就是所谓的数据一致性问题。讲解决数据一致性方案的文章比较多,我就不赘述了,其中的根本是CAP理论,大家可自行百度/Google下。总结一下一般在分布式场景中无非就是两种方式来解决:2阶段提交的强一致性(选择CP)或者最终一致性(选择AP)。2阶段提交大家都懂,是性能杀手,阻塞式的操作会导致整个系统的瓶颈提早到来。最终一致性是非阻塞式的异步机制,通过消息体在多个系统内流转,并各自根据消息体来处理不同的业务,并且最终一致性有很多种形式来实现,这里暂不展开讨论。
三、回到DDD
在DDD中实现最终一致性需要引入一个之前一直没提到的概念:领域事件。
问1:什么是领域事件?
答:领域事件是领域的一部分,表示领域中所发生的事情。
问2:它存在的作用是?
答:①作为实现最终一致性的载体
②解耦
③通过事件让不同的上下文分散处理下游业务,减少对数据的反向获取。处理单元更小化。
④对开闭原则(OCP:Open-Closed Principle)最好体现。
问3:那么我们如何运用到DDD中?
答:①哪怕是同一个上下文中的不同聚合也需要通过领域事件来进行同步。
②把领域事件设计成聚合,但是其中的大部分代表事件发生与过去的部分属性应该为只读。设计为聚合拥有了唯一标识这样便于跟踪事件、持久化和跨限界上下文交互。
③使用发布 —— 订阅的方式来处理事件,降低耦合。
④有时,有必要使用领域服务来注册事件订阅方。这样的动机可能和让应用服务来注册订阅方一样,但是此时我们可能有特定于领域的原因。
⑤领域事件的一个经验法则是这样的:领域事件中所包含的信息应该满足80%的消费方,虽然对于很多消费方来说,这些信息是多余的。
四、设计
根据上面的描述,设计了以下的几个对象进行实现领域事件的发布和订阅,如下图1:
【图1】
DomainEventBus是一个单例。事件(继承自DomainEvent)的发布全部经由它来处理,分发失败的时候会抛出一个DistributeExceptionEvent的事件,由调用方决定后续的处理方式。另外事件订阅者(继承自DomainEventSubscriber)也通过DomainEventBus来注册订阅。类型依赖图如下图2:
【图2】
五、实现
为了能够比较直观的表达当前这个提交订单业务操作的处理流程,我粗略画了个时序图,如下图3。
【图3】
这里的事件发布是订单上下文内的一个组件,是一个进程内操作。另外事件具体发布的目的地由不同的订阅者控制,暂时就列出了2个。
好了根据上面的时序图描述,下面贴出其中的核心代码:
1.事件订阅
).GetTypes().Where(ent => !ent.IsGenericType && ent.GetInterface(typeof(IDomainEventSubscriber).FullName) != null).ToList(); foreach (var type in types) { var subscriberInstance = Activator.CreateInstance(AppDomain.CurrentDomain, type.Assembly.FullName, type.FullName).Unwrap(); var subscriber = (IDomainEventSubscriber)subscriberInstance; DomainEventBus.Instance().Subscribe(subscriber); }
2.和2个对订单创建事件的订阅者
public class OrderCreatedSubscriberPaymentContext : DomainEventSubscriber<OrderCreated> { HandleEvent(OrderCreated domainEvent) { NotImplementedException(); } }
public class OrderCreatedSubscriberSellingPriceContext : DomainEventSubscriber<OrderCreated> { HandleEvent(OrderCreated domainEvent) { System.NotImplementedException(); } }
3.事件发布
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(); }