如何一步一步用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:

如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展

                             【图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:

如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展

                              【图2】

  其实很明显通过回滚的方式有很多局限性。所以说个人建议选择下面的方案,尽量做到内部消化,以提高接口对外的自治性。另外针对重试进行一些限制,一是为了减少一些无用功来占用系统资源,二是避免在系统本身达到瓶颈的情况下出现马太效应,让拥堵问题越发严重。

六、结语

  本篇没有增加太多代码,只是在Mall.Infrastructure中增加了几个工作单元(方式(2))相关的类,其中只包含了一些核心逻辑代码,具体的实现希望大家能够自己动手。多谢各位看官。

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

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

▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

上一篇:struts2 Unable to load configuration. - bean - jar:file:struts2-core-2.2.3.jar!/struts-default.xml:29:72


下一篇:Java 简单实用方法二