本系列所有文章

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

                             【图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
出处:https://zacharyfan.com/archives/199.html

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

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

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

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

如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展的更多相关文章

  1. 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

    阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...

  2. 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

    阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...

  3. 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

     阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...

  4. 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

    阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...

  5. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

  6. 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发

    阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...

  7. 如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户

    阅读目录 前言 怎么卖 领域服务的使用 回到现实 结语 一.前言 上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计.现在把剩余的“卖”这个动作给做了.这里提醒一下,正常情况下,我们的每一步业 ...

  8. 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域

    一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...

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

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

  10. 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

    阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...

随机推荐

  1. python redis list操作

    LPUSH list_name value [value ...] Prepend one or multiple values to a list 从左侧插入值,最早插入的值在最右边 LPUSHX ...

  2. Xcode的管理工具

    Xcode插件管理工具Alcatraz Alcatraz 1.简介 Alcatraz是一个能帮你管理Xcode插件丶模版及颜色配置的工具.它可以直接集成在Xcode的图形界面中,让你感觉就像在使用Xc ...

  3. (简单) POJ 1511 Invitation Cards,SPFA。

    Description In the age of television, not many people attend theater performances. Antique Comedians ...

  4. MySQL的mysql_insert_id和LAST_INSERT_ID

    摘要:mysql_insert_id和LAST_INSERT_ID二者作用一样,均是返回最后插入值的ID 值 1 mysql_insert_id 一.PHP获取MYSQL新插入数据的ID  mysql ...

  5. STM32 USB虚拟串口(转)

    源:STM32 USB虚拟串口 串口调试在项目中被使用越来越多,串口资源的紧缺也变的尤为突出.很多本本人群,更是深有体会,不准备一个USB转串口工具就没办法进行开发.本章节来简单概述STM32低端芯片 ...

  6. Servlet实现文件上传(简单)(一)

     1..使用到的jar包,为apache的一个子项目  此commons-fileupload-1.2.2需要以下commons-io-2.0.1的支持   2.页面展示fileUpload.jsp ...

  7. MySQL协议分析2

    MySQL协议分析 议程 协议头 协议类型 网络协议相关函数 NET缓冲 VIO缓冲 MySQL API 协议头 ● 数据变成在网络里传输的数据,需要额外的在头部添加4 个字节的包头. . packe ...

  8. CentOS根分区占满

    我中奖了!!! 查看硬盘的使用情况(bjchenxu)df -k 以K为单位显示df -h 以人性化单位显示,可以是b,k,m,g,t.. 查看各目录占用大小 du -sh * du -ms /*

  9. UVa 231 - Testing the CATCHER

    题目大意:一种拦截导弹能拦截多枚导弹,但是它在每次拦截后高度不会再升高,给出导弹的序列,问最多能拦截多少枚导弹? 最长递减子序列问题. #include <cstdio> #include ...

  10. eclipse中集成svn maven开发手册---导入项目

    一,导入项目 二,创建提分支 三,maven编译打包 四,合并主干 一,导入项目 右键,import,选择从svn检出项目 点击next,如图 如果是第一次导入,选择创建新的资源库,点击next,输入 ...