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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CSDN技术头条

架构之路(六):把框架拉出来

【编者按】本文作者自由飞,具有 传奇般的人生经历: 98年读大学-国际贸易专业 03年11月英语培训机构当英语老师 04年2月-05年6月律师...

1849
来自专栏Rainbond开源「容器云平台」

搬运向 | 浅析serverless架构与实践

8355
来自专栏HansBug's Lab

【备忘】Idea的那些事

说到Java的IDE,似乎eclipse和Idea是目前的主流。然而,OO的课程组却一直在推荐使用eclipse,于是很多人就这样错过了Idea这样强大的IDE...

4229
来自专栏开源优测

[大数据测试]ETL测试或数据仓库测试入门

概述 在我们学习ETL测试之前,先了解下business intelligence(即BI)和数据仓库。 什么是BI? BI(Business Intelli...

2734
来自专栏陈树义

JVM系列第2讲:Java 虚拟机的历史

说起 Java 虚拟机,许多人就会将其与 HotSpot 虚拟机等同看待。但实际上 Java 虚拟机除了 HotSpot 之外,还有 Sun Classic V...

912
来自专栏跨界架构师

如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备

最近实在太忙,上周停更了一周。按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理。从整个流程来看,这里需要用户填写的信息是最多的,那么在后...

1633
来自专栏chafezhou

分享自用的两个网站

1534
来自专栏Crossin的编程教室

工欲善其事必先利其器:用什么写Python?

通常来说,每个程序员都有自己趁手的兵器:代码编辑器。你要是让他换个开发环境,恐怕开发效率至少下降三成。然而,每个人对编辑器的喜好各不相同,甚至引发出诸如“神的编...

1272
来自专栏web前端教室

领读《深入浅出NODEJS》—快速阅读第二章

image.png 昨天跟大家介绍了2.2 node的模块实现,这一章节的内容。今天我们继续往下看,这本书到目前为止,写的都是偏向理论的东西,也许它整本书都是这...

1856
来自专栏张善友的专栏

DinnerNow.net: 微软最新技术集成示例

DinnerNow.net是微软推出的一个网站, 该网站尽可能地集成了微软的最新技术和产品,像IIS 7, WCF, WF, WPF, LINQ, Window...

1996

扫码关注云+社区