如何一步一步用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设计一个电商网站(十三)—— 领域事件扩展的更多相关文章
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
- 如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
阅读目录 前言 怎么卖 领域服务的使用 回到现实 结语 一.前言 上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计.现在把剩余的“卖”这个动作给做了.这里提醒一下,正常情况下,我们的每一步业 ...
- 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域
一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...
- 如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
阅读目录 前言 准备 实现 结语 一.前言 最近实在太忙,上周停更了一周.按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理.从整个流程来看,这里需要用户填写的信息是最多的,那么 ...
- 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...
随机推荐
- ARM架构解析
ARM架构解析 (2014-11-23 21:56:53) 转载▼ 标签: francis_hao arm架构 arm核 soc 分类: MCU 先来谈一下ARM的发展史:1978年12月5日,物理学 ...
- nexus 中央仓库
nexus 中央仓库 下载地址:http://www.sonatype.org/nexus/archived 下载最新版本 mkdir -p /opt/local/nexus tar zxvf nex ...
- [即时通讯]openfire启动失败解决办法
当你发现你的电脑上的openfire无论你是重新安装还是重启都无法启动的情况下你可以按照我下面写的那些终端指令来启动你的openfire不用再去重新做你的系统了. 一般你发现你的openfire打开出 ...
- onchange、oninput、onpropertyChange事件的异同
onchange事件适用于input\textarea\select元素上,支持各大浏览器,该事件只有在键盘或者鼠标操作改变对象属性,且失去焦点时触发,脚本触发无效: oninput事件适用于inpu ...
- tp框架实现验证码验证
//实现验证页面 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://ww ...
- double和real型有什么区别 [
DOUBLE是双精度浮点数REAL 是实数类型,他包括 DOUBLE,SINGLE等类型
- 最新 Spring 4.2.2 集成 Quartz Scheduler 2.2.2 任务调度示例
参考http://blog.csdn.net/defonds/article/details/49496895 本文将演示如何通过 Spring 使用 Quartz Scheduler 进行任务调度. ...
- IOS第三方数据库--FMDB 分类: ios技术 2015-03-01 09:38 57人阅读 评论(0) 收藏
iOS中原生的SQLite API在使用上相当不友好,在使用时,非常不便.于是,就出现了一系列将SQLite API进行封装的库,例如FMDB.PlausibleDatabase.sqlitepers ...
- 如何在Eclipse下安装myeclipse插件
来自http://www.blogjava.net/show911/archive/2008/04/27/86284.html 下载myeclipse插件 支持eclipse3.1.x, 具体安装步骤 ...
- Centos5.5系统备份
使用root用户切换到根目录 然后,使用下面的命令备份完整的系统: tar cvpzf backup.tgz / --exclude=/proc --exclude=/lost+found --exc ...