订单处理减库存的设计

订单处理减库存的设计

前言

前面的文章,我介绍了Conference案例的业务、上下文划分、领域模型、架构,以及代码整体流程。接下来想针对案例中一些重要的场景,分别做进一步的分析。本文想先介绍一下Conference案例的核心业务场景 - 订单处理减库存的设计。

下单以及订单处理流程描述

  1. 下单过程

    • 预订者浏览某个已发布的会议;
    • 进入会议的详情页面,该页面显示了所有可预订的座位分类信息;
    • 预订者选择好要预订的座位分类,录入每个分类的预定数量;
    • 预订者点击提交按钮,提交下单请求到Server端;
  2. Server端订单处理过程
    • Server端Controller提交处理订单的命令到分布式消息队列(EQueue),然后后台的Command Processor就可以消费该命令并异步处理订单了。核心处理步骤:

      • 生成订单(初始状态);
      • 扣减库存(内部有预扣逻辑);
      • 更新订单状态;
    • Server端Controller发送命令后,立即重定向页面到查单订单处理结果页面,该页面会以轮训的方式查看订单处理结果;
  3. 用户等待订单处理结果
    • 如果下单成功(库存足够),预订者被导航到支付页面进行支付;预订者可以选择支付,也可以放弃支付;
    • 如果下单失败(库存不足),则提示用户下单失败,因为库存不足;
    • 如果轮训等待超时,则告诉用户暂时无法知道订单处理状态,然后当前页面继续定时(5s)轮训订单处理结果;
  4. 用户支付订单
    • 如果支付成功,则提示预订者订单处理完成,交易完成;
    • 如果拒绝支付,则关闭订单;
    • 如果超过规定时间(15分钟)未支付,则视作订单已过期,系统自动回收订单所预定的座位;
  5. 流程结束;

上面用文字描述了整个下单和订单处理以及支付的过程,而我们实际关心的核心还是服务端对订单处理的过程。所以下面我们就看看如何来进行代码实现。

订单处理Saga流程图

Conference案例中,服务端处理订单是采用CQRS Saga流程的方式实现的。一个Saga流程是一个事件驱动的业务流程,它的周期可能比较长,因为流程中某些步骤需要用户参与的。上图描述了服务端处理订单的正常处理逻辑。为什么说是正常处理逻辑,因为实际的代码比上面的流程图还要复杂一点,上面的流程图中没有画出库存不足、用户拒绝付款、或者付款超时等情况的处理。我觉得这些特殊的情况,只要读者自己看一下代码就能很快理解了。只要我们能够把正常的逻辑搞清楚,那我们心里就对整个订单处理的流程有了解了。

上图中,聚合根之间棕色的箭头表示Command,蓝色的箭头表示Event。Order Process Manager表示一个无状态的Saga流程管理器,它负责协调其他有状态的聚合根,负责整个订单处理的流程控制逻辑。从代码表现上来看,它的任务就是响应Event,然后发出下一个Command。然后Order, Conference, Payment三个聚合根分别表示订单、会议、支付。这三个聚合根分别封装自己的状态和业务规则。

订单处理之减库存的设计思路

库存信息在哪里维护

大家都知道,电子商务系统,订单处理时,核心的环节就是减库存。那我们首先要思考的问题是,库存在哪里维护呢?在我看了微软的Conference案例的代码后,发现它的库存信息是在Registration(订单处理)的上下文中维护的。当ConferenceManagement(会议管理)上下文中,对会议的库存有修改时,会通过事件异步同步到订单处理上下文。我在思考它这样设计的理由是什么,我能想到的唯一理由是,这样的好处是减库存时,就只需要在Registration当前的上下文中处理即可,这样就不需要依赖会议管理上下文了。但代价就是需要从会议管理上下文同步库存信息。

我的思考:

我个人认为,库存信息还是应该在会议管理上下文中维护比较合理,因为会议管理上下文的职责就是维护会议的基本信息以及会议的座位类型的实体信息。如果我们的库存管理没有独立为独立的上下文,那最合理的维护地方就是会议管理上下文。这样,一份数据就只需要在一个地方维护,不需要同步。然后当订单处理上下文需要减库存时,可以通过远程调用或者异步消息通信的方式来实现上下文之间的交互。

但实际的电商系统,比如像淘宝这种,由于库存管理也是一块复杂的业务,所以一般会独立出一个上下文,叫库存中心。然后这个库存中心独立于商品中心以及订单中心。当订单中心要求减库存时,只需要和库存中心进行交互即可。这样的方式,会让系统的职责更明确,商品中心不需要关心商品的库存了,只需要关注商品本身的属性信息即可。然后,本案例由于只是案例,所以没有独立出库存中心,即库存上下文。所以会议座位的库存管理放在会议管理上下文中。

库存怎么预扣

明确了库存所属的上下文后,我们接下来思考怎么实现减库存。减库存主要的问题是,在并发减库存的情况下,可能会出现超卖的情况。为了解决超卖的问题,一般主流的做法是采用预扣库存的方式,类似分布式事务的二阶段提交的过程。预扣的意思是先预扣库存,如果预扣成功,就可以通知用户下单成功,然后就可以让用户去付款了;如果预扣时发现库存不足,则提示用户库存不足。

然后,虽然是预扣,但因为大家同时预扣同一个会议聚合根的座位库存,所以还是会产生并发问题。但由于我们操作的是同一个聚合根,所以ENode框架帮我们确保不会有并发问题。我们先看看Conference聚合根内部关于座位的库存管理的设计实现。

如上面的代码所示,Conference聚合根里聚合了所有的座位类型子实体,每个座位类型维护了座位的名称、价格、数量;然后Conference聚合根里还维护了所有的预定记录,这个应该不难理解。MakeReservation方法就是Conference聚合根对外提供预定座位支持的方法。该方法接收一些要预定的项,以及一个预定的ID,表示这次预定是谁(实际上就是订单ID)要预定。该方法内部的逻辑是:

  1. 判断当前会议是否没有发布,如果没有发布,那是不允许预定的;
  2. 判断这个预定(reservationId)是否是重复预定,如果重复,也会抛出异常;为什么会出现重复预定,因为订单处理上下文是通过发送命令的方式来通知Conference进行预定的,而由于是分布式消息队列(EQueue),所以命令可能会被重复执行。
  3. 检查预定的座位明细是否为空,如果为空,就认为是无效的预定,抛异常;
  4. 接下来就是循环处理每个预定项,先检查预定项本身需要预定的数量是否无效(小于等于0),如果无效,则抛出异常;再从Conference聚合根里找到当前要预定的座位类型子实体;然后计算当前的座位类型是否有足够的可用库存,GetTotalReservationQuantity方法就是计算当前该座位类型总共已经预定的总数。如果库存不足,则直接抛出异常。当然这里直接抛出异常可能还是太草率了一点。因为真正的电子商务系统,应该会明确提示用户,哪些商品库存不足,是否要修改订单只购买剩余的库存。本案例为了让代码不会太复杂,所以简化了功能。只要被预定的座位类型出现一个库存不足,就认为下单失败了。
  5. 当所有的预定项都处理完成后,就可以产生“已预定”的领域事件了。注意,这里我们产生事件的时候,同时把当前每个座位类型剩余的库存数量也放在领域事件里了。这样的好处是,当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而已。

结束语

好了,本文基本把订单处理的核心环节减库存讲了一下,本来还想再结合订单状态的变更讲一下订单状态在这个过程中是如何变化的。但由于今天时间比较完了,不准备讲了。我在前面的领域模型的介绍中,已经基本讲了。

分类: DDD 案例分析

时间: 2024-11-06 14:49:33

订单处理减库存的设计的相关文章

ENode框架Conference案例分析系列之 - 订单处理减库存的设计

前言 前面的文章,我介绍了Conference案例的业务.上下文划分.领域模型.架构,以及代码整体流程.接下来想针对案例中一些重要的场景,分别做进一步的分析.本文想先介绍一下Conference案例的核心业务场景 - 订单处理减库存的设计. 下单以及订单处理流程描述 下单过程 预订者浏览某个已发布的会议: 进入会议的详情页面,该页面显示了所有可预订的座位分类信息: 预订者选择好要预订的座位分类,录入每个分类的预定数量: 预订者点击提交按钮,提交下单请求到Server端: Server端订单处理过

订单减库存设计

一.扣减库存的三种方案 (1)下单减库存 用户下单时减库存 优点:实时减库存,避免付款时因库存不足减库存的问题 缺点:恶意买家大量下单,将库存用完,但是不付款,真正想买的人买不到 (2)付款减库存 下单页面显示最新的库存,下单时不会立即减库存,而是等到支付时才会减库存. 优点:防止恶意买家大量下单用光库存,避免下单减库存的缺点 缺点:下单页面显示的库存数可能不是最新的库存数,而库存数用完后,下单页面的库存数没有刷新,出现下单数超过库存数,若支付的订单数超过库存数,则会出现支付失败. (3)预扣库

EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题

下订单减库存的方式 现在,连农村的大姐都会用手机上淘宝购物了,相信电商对大家已经非常熟悉了,如果熟悉电商开发的同学,就知道在买家下单购买商品的时候,是需要扣减库存的,当然有2种扣减库存的方式, 一种是预扣库存,相当于锁定库存, 一种是直接扣减库存. 我们采用的是预扣库存的方式,预扣库存的时候,在SalesInfo表中,将最大可售数量MaxSalesNum减去购买数量,用一条SQL语句来表示这个业务,就是下面这个样子的: update salesinfo set MaxSalesNum=MaxSa

28、生鲜电商平台-库存管理设计与架构

说明:Java开源生鲜电商平台-库存管理设计与架构有以下几个功能 WMS的功能:1.业务批次管理 该功能提供完善的物料批次信息.批次管理设置.批号编码规则设置.日常业务处理.报表查询,以及库存管理等综合批次管理功能,使企业进一步完善批次管理,满足经营管理的需求. 2.保质期管理在批次管理基础上,针对物料提供保质期管理及到期存货预警,以满足食品和医yao行业的保质期管理需求.用户可以设置保质期物料名称.录入初始数据.处理日常单据,以及查询即时库存和报表等. 3.质量检验管理集成质量管理功能是与采购

Oracle EBS-SQL (OM-3):销售连接停靠站时冲减库存出错处理.sql

DELETE FROM INV.MTL_RESERVATIONS MRWHERE EXISTS (SELECT 1 FROM WSH.WSH_DELIVERY_ASSIGNMENTS WDA                            ,WSH.WSH_DELIVERY_DETAILS    WDD                 WHERE WDA.DELIVERY_DETAIL_ID = WDD.DELIVERY_DETAIL_ID                     AND

[转] 基于MySQL的秒杀核心设计(减库存部分)-防超卖与高并发

商品详情页面的静态化,varnish加速,秒杀商品库独立部署服务器这种就略过不讲了.只讨论库存部分的优化 mysql配置层面的优化可以参考我的这篇文章 <关于mysql innodb引擎性能优化的一点心得> 重点设计在数据库层面. 2张表: 第一张:判重表(buy_record),该用户有没秒杀过该商品 字段: id, uid, goods_id, addtime 第二张表:商品表 goods 字段: goods_id   goods_num 方案1: start transaction; s

关于杜绝减库存为负的情况

1  我们在执行sql语句的时候,如果我们在一个事务里执行到查询一条语句之后在更新保存.这样会出现库存为负的情况: 因为我们查询用的是共享锁,你在读的时候其他人也在读,你们读取相同的数据这样不能保证数据的准确性.可以采取两种方法解决这个问题, (1)执行过更新之后再执行一次查询看一下是否为负,是负的情况回滚事务. (2)采用锁的方式,在更新是根据索引锁住当前行其他人不能再进行修改.

[原创]商城系统下单库存管控系列杂记(一)(并发安全和性能基础认识)

商城系统下单库存管控系列杂记(一)(并发安全和性能基础认识) 前言 参与过几个中小型商城系统的开发,随着时间的增长,以及对系统的深入研究和测试,发现确实有很多值得推敲和商榷的地方(总有很多重要细节存在缺陷).基于商城系统,无论规模大小,或者本身是否分布架构,个人觉得最核心的一环就是下单模块,而这里面更相关和棘手的一些设计和问题,大多时候都涉及库存系统.想想之前跟某人的交流,他一句"库存管控做得好,系统设计就成功了一半",自己颇有认同.围绕这个点,结合目前经验和朋友间的交流(包括近来参阅

Java生鲜电商平台-电商订单系统全解析

Java生鲜电商平台-电商订单系统全解析 今天分享将会分为以下三个环节来阐述: 1.订单系统的介绍 2.订单系统的解构 3.垂直电商订单系统设计思路 一.什么是订单系统? 订单管理系统(OMS)是物流管理系统的一部分,通过对客户下达的订单进行管理及跟踪,动态掌握订单的进展和完成情况,提升物流过程中的作业效率,从而节省运作时间和作业成本,提高物流企业的市场竞争力.顾名思义,电商系统就是用户.平台.商户等对于订单的管控.跟踪的系统,衔接着商品中心.wms.促销系统.物流系统等,是电子商务的基础模块: