ENode框架Conference案例分析系列之 - 订单处理减库存的设计
前言
前面的文章,我介绍了Conference案例的业务、上下文划分、领域模型、架构,以及代码整体流程。接下来想针对案例中一些重要的场景,分别做进一步的分析。本文想先介绍一下Conference案例的核心业务场景 - 订单处理减库存的设计。
下单以及订单处理流程描述
- 下单过程
- 预订者浏览某个已发布的会议;
- 进入会议的详情页面,该页面显示了所有可预订的座位分类信息;
- 预订者选择好要预订的座位分类,录入每个分类的预定数量;
- 预订者点击提交按钮,提交下单请求到Server端;
- Server端订单处理过程
- Server端Controller提交处理订单的命令到分布式消息队列(EQueue),然后后台的Command Processor就可以消费该命令并异步处理订单了。核心处理步骤:
- 生成订单(初始状态);
- 扣减库存(内部有预扣逻辑);
- 更新订单状态;
- Server端Controller发送命令后,立即重定向页面到查单订单处理结果页面,该页面会以轮训的方式查看订单处理结果;
- Server端Controller提交处理订单的命令到分布式消息队列(EQueue),然后后台的Command Processor就可以消费该命令并异步处理订单了。核心处理步骤:
- 用户等待订单处理结果
- 如果下单成功(库存足够),预订者被导航到支付页面进行支付;预订者可以选择支付,也可以放弃支付;
- 如果下单失败(库存不足),则提示用户下单失败,因为库存不足;
- 如果轮训等待超时,则告诉用户暂时无法知道订单处理状态,然后当前页面继续定时(5s)轮训订单处理结果;
- 用户支付订单
- 如果支付成功,则提示预订者订单处理完成,交易完成;
- 如果拒绝支付,则关闭订单;
- 如果超过规定时间(15分钟)未支付,则视作订单已过期,系统自动回收订单所预定的座位;
- 流程结束;
上面用文字描述了整个下单和订单处理以及支付的过程,而我们实际关心的核心还是服务端对订单处理的过程。所以下面我们就看看如何来进行代码实现。
订单处理Saga流程图
Conference案例中,服务端处理订单是采用CQRS Saga流程的方式实现的。一个Saga流程是一个事件驱动的业务流程,它的周期可能比较长,因为流程中某些步骤需要用户参与的。上图描述了服务端处理订单的正常处理逻辑。为什么说是正常处理逻辑,因为实际的代码比上面的流程图还要复杂一点,上面的流程图中没有画出库存不足、用户拒绝付款、或者付款超时等情况的处理。我觉得这些特殊的情况,只要读者自己看一下代码就能很快理解了。只要我们能够把正常的逻辑搞清楚,那我们心里就对整个订单处理的流程有了解了。
上图中,聚合根之间棕色的箭头表示Command,蓝色的箭头表示Event。Order Process Manager表示一个无状态的Saga流程管理器,它负责协调其他有状态的聚合根,负责整个订单处理的流程控制逻辑。从代码表现上来看,它的任务就是响应Event,然后发出下一个Command。然后Order, Conference, Payment三个聚合根分别表示订单、会议、支付。这三个聚合根分别封装自己的状态和业务规则。
订单处理之减库存的设计思路
库存信息在哪里维护
大家都知道,电子商务系统,订单处理时,核心的环节就是减库存。那我们首先要思考的问题是,库存在哪里维护呢?在我看了微软的Conference案例的代码后,发现它的库存信息是在Registration(订单处理)的上下文中维护的。当ConferenceManagement(会议管理)上下文中,对会议的库存有修改时,会通过事件异步同步到订单处理上下文。我在思考它这样设计的理由是什么,我能想到的唯一理由是,这样的好处是减库存时,就只需要在Registration当前的上下文中处理即可,这样就不需要依赖会议管理上下文了。但代价就是需要从会议管理上下文同步库存信息。
我个人认为,库存信息还是应该在会议管理上下文中维护比较合理,因为会议管理上下文的职责就是维护会议的基本信息以及会议的座位类型的实体信息。如果我们的库存管理没有独立为独立的上下文,那最合理的维护地方就是会议管理上下文。这样,一份数据就只需要在一个地方维护,不需要同步。然后当订单处理上下文需要减库存时,可以通过远程调用或者异步消息通信的方式来实现上下文之间的交互。
但实际的电商系统,比如像淘宝这种,由于库存管理也是一块复杂的业务,所以一般会独立出一个上下文,叫库存中心。然后这个库存中心独立于商品中心以及订单中心。当订单中心要求减库存时,只需要和库存中心进行交互即可。这样的方式,会让系统的职责更明确,商品中心不需要关心商品的库存了,只需要关注商品本身的属性信息即可。然后,本案例由于只是案例,所以没有独立出库存中心,即库存上下文。所以会议座位的库存管理放在会议管理上下文中。
我当初看微软的例子,第一反应就觉得把库存放到订单上下文不合理,因为我没见过这样的设计。然后我看到会议管理上下文里,它也对会议作为的库存做了管理,而且是源头(库存的第一手数据在会议管理上下文产生),另外,会议管理上下文还会发布会议。所以,这些都让我意识到,会议管理就是商品中心和库存中心的结合体。但是让我费解的是,微软自己自相矛盾了,居然为了bc之间尽量解耦,居然把库存信息同步到订单上下文了。这样的设计导致代码非常丑陋,我认为再怎么样也不能把库存放到订单上下文里。所以,最后才有了我的enode的conference这样的bc的划分的考虑。再联想到阿里的电商平台,库存上下文是独立于订单上下文的。而我这里的实现,只是偷懒了(因为只是案例),没有把库存上下文独立出来而已。
所以,库存上下文是合并到订单上下文比起合并到商品中心上下文更不合理。
库存怎么预扣
明确了库存所属的上下文后,我们接下来思考怎么实现减库存。减库存主要的问题是,在并发减库存的情况下,可能会出现超卖的情况。为了解决超卖的问题,一般主流的做法是采用预扣库存的方式,类似分布式事务的二阶段提交的过程。预扣的意思是先预扣库存,如果预扣成功,就可以通知用户下单成功,然后就可以让用户去付款了;如果预扣时发现库存不足,则提示用户库存不足。
然后,虽然是预扣,但因为大家同时预扣同一个会议聚合根的座位库存,所以还是会产生并发问题。但由于我们操作的是同一个聚合根,所以ENode框架帮我们确保不会有并发问题。我们先看看Conference聚合根内部关于座位的库存管理的设计实现。
如上面的代码所示,Conference聚合根里聚合了所有的座位类型子实体,每个座位类型维护了座位的名称、价格、数量;然后Conference聚合根里还维护了所有的预定记录,这个应该不难理解。MakeReservation方法就是Conference聚合根对外提供预定座位支持的方法。该方法接收一些要预定的项,以及一个预定的ID,表示这次预定是谁(实际上就是订单ID)要预定。该方法内部的逻辑是:
- 判断当前会议是否没有发布,如果没有发布,那是不允许预定的;
- 判断这个预定(reservationId)是否是重复预定,如果重复,也会抛出异常;为什么会出现重复预定,因为订单处理上下文是通过发送命令的方式来通知Conference进行预定的,而由于是分布式消息队列(EQueue),所以命令可能会被重复执行。
- 检查预定的座位明细是否为空,如果为空,就认为是无效的预定,抛异常;
- 接下来就是循环处理每个预定项,先检查预定项本身需要预定的数量是否无效(小于等于0),如果无效,则抛出异常;再从Conference聚合根里找到当前要预定的座位类型子实体;然后计算当前的座位类型是否有足够的可用库存,GetTotalReservationQuantity方法就是计算当前该座位类型总共已经预定的总数。如果库存不足,则直接抛出异常。当然这里直接抛出异常可能还是太草率了一点。因为真正的电子商务系统,应该会明确提示用户,哪些商品库存不足,是否要修改订单只购买剩余的库存。本案例为了让代码不会太复杂,所以简化了功能。只要被预定的座位类型出现一个库存不足,就认为下单失败了。
- 当所有的预定项都处理完成后,就可以产生“已预定”的领域事件了。注意,这里我们产生事件的时候,同时把当前每个座位类型剩余的库存数量也放在领域事件里了。这样的好处是,当Q端的Event Handler在更新Conference的读库时,不需要再计算了,直接用Update语句更新DB即可。这个设计大家可以参考下,这样的设计,体现了Domain中封装了一切数据更新的业务规则判断和逻辑处理,然后通过事件的方式通知Domain外部当前事件发生后,聚合根的当前状态(一定是第一手数据,不会是脏数据)是什么。这样外部的Event Handler的逻辑就非常简单了,都只需要简单的用Update语句更新DB即可(不用动脑子,呵呵)。
并发问题的处理
Domain不会考虑并发这种技术问题,它只关心自己的业务规则和数据一致性,完全从业务角度来写代码。我们可以看到Conference聚合根里封装了很多的规则和逻辑。然后Conference聚合根产生的Event持久化到EventStore时的并发问题,ENode框架会帮我们解决,应用开发者不用担心了。如果大家关心是怎么解决的,可以去看一下ENode我以前写过的一些介绍,核心思路是乐观并发控制(通过聚合根版本号)+ 自动重试的机制,这里我就不展开了。
通过上面的设计,我们知道每次预扣时总是会判断当前可用的库存,并且已经考虑了其他已经预扣了的订单;这就从业务逻辑上保证了不会出现超卖;然后ENode框架解决了并发问题,所以最后我们可以确保一定不会出现超卖的情况。
用户付款后怎么真正减库存
当预扣成功后,用户就会去付款,假如付款成功了,那系统就会自动提交之前的预扣记录,做真正的减库存。我们来看看逻辑是怎么样的:
CommitReservation方法是Conference聚合根用来提供支持提交减库存的方法。它接收一个要提交减库存的reservationId,通过该ID,先找到之前它预定的所有预定项,然后产生一个事件,事件中包含每个预定项所对应的座位类型的扣除后的库存数量,最后产生领域事件。然后聚合根内部会响应领域事件,更新聚合根自己的状态。我们在Commit阶段是不用担心数据有什么问题的,因为肯定是之前预扣过了,只要预扣记录存在,那就可以放心的做减库存逻辑的。这是我们通过业务上的2PC协议保证的。
代码很直接,就是先删除预定记录,并把预定记录的每个明细对应的座位类型的库存更新即可。然后,我们的读库的更新也是这样的逻辑,只是更新的是读库DB而已。
结束语
好了,本文基本把订单处理的核心环节减库存讲了一下,本来还想再结合订单状态的变更讲一下订单状态在这个过程中是如何变化的。但由于今天时间比较完了,不准备讲了。我在前面的领域模型的介绍中,已经基本讲了。
ENode框架Conference案例分析系列之 - 订单处理减库存的设计的更多相关文章
- ENode框架Conference案例分析系列之 - 复杂情况的读库更新设计
问题背景 Conference案例,是一个关于在线创建会议(类似QCon这种全球开发者大会).在线管理会议位置信息.在线预订某个会议的位置的,这样一个系统.具体可以看微软的这个项目的主页:http:/ ...
- ENode框架Conference案例分析系列之 - 文章索引
ENode框架Conference案例分析系列之 - 业务简介 ENode框架Conference案例分析系列之 - 上下文划分和领域建模 ENode框架Conference案例分析系列之 - 架构设 ...
- ENode框架Conference案例分析系列之 - 架构设计
Conference架构概述 先贴一下Conference案例的在线地址,UI因为完全拿了微软的实现,所以都是英文的,以后我有空再改为中文的. Conference后台会议管理:http://www. ...
- ENode框架Conference案例分析系列之 - 上下文划分和领域建模
前面一片文章,我介绍了Conference案例的核心业务,为了方便后面的分析,我这里再列一下: 业务描述 Conference是这样一个系统,它提供了一个在线创建会议以及预订会议座位的平台.这个系统的 ...
- ENode框架Conference案例分析系列之 - Quick Start
前言 前一篇文章介绍了Conference案例的架构设计,本篇文章开始介绍Conference案例的代码实现.由于代码比较多,一开始就全部介绍所有细节,估计很多人接受不了,也理解不了.所以,我先进行一 ...
- ENode框架Conference案例分析系列之 - ENode框架初始化
前言 Conference案例是使用ENode框架来开发的.之前我没有介绍过ENode框架是如何启动的,以及启动时要注意的一些点,估计很多人对ENode框架的初始化这一块感觉很复杂,一头雾水.所以,本 ...
- ENode框架Conference案例分析系列之 - 事件溯源如何处理重构问题
前言 本文可能对大多数不太了解ENode的朋友来说,理解起来比较费劲,这篇文章主要讲思路,而不是一上来就讲结果.我写文章,总是希望能把自己的思考过程尽量能表达出来,能让大家知道每一个设计背后的思考的东 ...
- ENode框架Conference案例分析系列之 - 业务简介
前言 ENode是一个应用开发框架.通过ENode,我们可以方便的开发基于DDD+CQRS+EventSourcing+EDA架构的应用程序.之前我已经写了很多关于ENode的架构以及设计原理的文章, ...
- ENode框架Conference案例转载
ENode框架Conference案例分析系列之 - Quick Start 前言 前一篇文章介绍了Conference案例的架构设计,本篇文章开始介绍Conference案例的代码实现.由于代码比较 ...
随机推荐
- MongoDB高可用集群配置的方案
>>高可用集群的解决方案 高可用性即HA(High Availability)指的是通过尽量缩短因日常维护操作(计划)和突发的系统崩溃(非计划)所导致的停机时间,以提高系统和应用的可用性. ...
- Flume(2)组件概述与列表
上一节搭建了flume的简单运行环境,并提供了一个基于netcat的演示.这一节继续对flume的整个流程进行进一步的说明. 一.flume的基本架构图: 下面这个图基本说明了flume的作用,以及f ...
- win7(x64)下安装cocos2d并编译安卓项目
好吧,不为啥,就是如题. win7 x64 脑袋内存比较小,说不定明儿就忘了,今天记录一下. 没有什么经验,所有步骤基本都是百度出来的,这里边操作边记录,为了保护原创作者,这里我都附上我查找的链接. ...
- SOUI中做的一个磁力吸附效果
代码见SVN
- 6 Candy_Leetcode
There are N children standing in a line. Each child is assigned a rating value. You are giving candi ...
- Git使用笔记
Ubuntu下安装步骤 sudo apt-get install git完成git的安装 安装完成后进行配置 git config –global user.name “Your Name” git ...
- nodejs持续学习--必须关注4网站
1.官网:https://nodejs.org/en/ 2.模块社区:www.npmjs.com(FQ) 3.github.com 4.全球技术问题讨论社区 http://stackoverflow. ...
- trigger() --工作中问题nav样式
自动执行某元素的某个事件 $("#div").trigger("click"); //让系统自动执行单击事件 适用于nav样式中,下面横线绝对定位于nav.o ...
- Python学习日志(二)
在网易云课堂看到小甲鱼的python视频,想起以前看就是看他的视频学C的虽然后来不了了之都怪我自己啦,于是决定跟着这个视频来学python啦! IDLE IDLE其实是一个python shell , ...
- [资料分享]SQL Server 2016/2014/2012/2008简体中文企业版下载+对应补丁
为什么只提供企业版下载呢?因为不管你是学生还是工作研究人员,企业版都是功能最为齐全的一个版本,比如企业版都集成了SQL Server Management Studio管理界面(俗称企业管理器的可视化 ...