25. 谷粒商城订单系统

简介

电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。

订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

订单构成

25. 谷粒商城订单系统_第1张图片

1、 用户信息
用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级
还可以获取积分的奖励等

2 、订单基础信息订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。

(1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
(2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
(5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等

3、 商品信息

商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。

4、优惠信息
优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

5、支付信息
(1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
(2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
(3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。用户实付金额=商品总金额+运费-优惠总金额

6、物流信息
物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点

订单状态

  1. 待付款
    用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
  2. 已付款/ 待发货
    用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS系统,仓库进行调拨,配货,分拣,出库等操作。
  3. 待收货/ 已发货
    仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
  4. 已完成
    用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
  5. 已取消
    付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
  6. 售后中
    用户在付款后申请退款,或商家发货后用户申请退换货。售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

订单流程

25. 谷粒商城订单系统_第2张图片

  1. 远程调用会员服务

    1. 根据会员id查询地址 √
  2. 远程调用购物车

    1. 获取当前用户的所有选中购物项:current-user-cart-items
    2. 需要获取每个购物项的最新价格
  3. 查询用户积分

  4. 计算应付、总付

  5. 防重令牌

Feign远程调用丢失请求头问题

25. 谷粒商城订单系统_第3张图片

解决

加上 feign 远程调用的请求拦截器,在每次发送远程请求之前,把老请求的数据同步过来,这样就可以解决请求头的丢失问题了。

异步任务丢失上下文问题

25. 谷粒商城订单系统_第4张图片

获取address、cart所使用的线程,与主任务的线程不同,所以异步任务无法获取主任务的上下文环境。

解决

将主线程中原始请求的上下文数据共享出来,在新开启的异步任务中重新设置上下文数据,即可解决

下单流程

25. 谷粒商城订单系统_第5张图片

25. 谷粒商城订单系统_第6张图片

锁库存流程

25. 谷粒商城订单系统_第7张图片

本地事务在分布式下的问题

1、假失败

如果保存订单成功,远程锁库存假失败,那就会出现问题

假失败就是我们在订单服务调库存服务时, 库存锁定成功,然后由于服务器慢、卡顿、等故障原因,本地事务提交了之后,一直没返回到订单服务

此时再看订单服务,因为调用库存服务时间太长了,库存服务迟迟没有返回结果,可能就会触发 feign 的超时机制,在调用远程服务这里抛异常:read time out 读取超时,但是这个异常并不是我们手动抛的锁库存异常,而是 feign 的异常

并且订单服务,设计的回滚机制,是只要一出现异常就会全部回滚,

结果:库存锁定成功,订单服务因为 feign 的超时机制,出现异常,导致订单数据全部回滚,最终数据不一致

2、调用新服务出现异常之后,已经执行的服务不会回滚

假设库存锁定成功,将结果返回到了订单服务,我们根据结果又调用了积分服务,让它扣减积分,

结果积分服务内部出现异常,积分数据回滚

此时再看订单服务,订单服务感知到我们手动抛的积分异常,订单数据回滚,但是库存服务,却不会有任何感知,

结果:积分、订单数据全部回滚,库存给锁定了,也是数据不一致

只需要在订单服务的库存执行成功之后,添加一个 int i = 10 / 0;,模拟积分服务出现异常,很容易就能复现这个问题

总结

本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚

产生分布式事务的最大原因,就是网络问题 + 分布式机器

如何解决下单系统这个高并发里边的分布式事务呢?

首先,我们肯定不会用 2PC 模式、 TCC-事务补偿性方案,我们也不考虑

最终我们选择了可靠消息+最终一致性这种方式

为了保证高并发,订单这一块还是自己回滚,

库存服务自己怎么回滚?

有两种解决办法

第一种

我们在提交订单那里,当捕捉到异常要回滚的时候,给库存服务发一个消息,让库存服务自己把库存解锁

这样不需要让库存事务回滚,只需要给它发一个消息,不会损失什么性能

第二种

库存服务本身也可以使用自动解锁模式。

怎么自动解锁呢?

需要使用消息队列来完成。

25. 谷粒商城订单系统_第8张图片

如果你想让我这的哪些库存解锁,首先你需要给我发一个消息告诉我。

然后我们专门的库存解锁服务,去来订阅我们stock.release.stock.queue这个队列里的消息。

那你给我发消息的时候,比如:用路由键stock.release,我知道要库存解锁,

然后,你的消息发给我们这个交换机stock-event-exchange

交换机把这个消息路由给stock.release.stock.queue这个队列。

然后,这个队列stock.release.stock.queue里边存的这些消息都是库存要解锁的消息,我们的库存解锁服务只要收到了,它就会在后台慢慢的解锁消息。

我们不用保证强一致,我们哪怕是二十分钟、三十分钟,乃至于一天以后把这个库存解锁了,最终一致了就行。

所以我们可以来使用消息队列来完成我们的这个最终一致性。

锁库存的增强版逻辑

25. 谷粒商城订单系统_第9张图片

我们想要锁库存的话,我们先来保存一个库存工作单和库存工作单详情

相当于只要我们想要锁库存,我们先给数据库里边保存记录,我要锁库存。

接下来我们就来进行锁,只要锁成功了,那一切ok。

如果锁失败了,数据库里边相当于没有这个锁库存记录。

因为锁失败呢,我们这个本身自己所失败会全部回滚。

但如果可能是这种失败,比如我们来到订单里边,我们库存其实自己锁成功了。但是我们订单下边的其他完了,然后库存要进行解锁。那怎么办呢?

我们可以使用定时任务

订单服务的完整消息队列

25. 谷粒商城订单系统_第10张图片

库存自动解锁

25. 谷粒商城订单系统_第11张图片

库存微服务,有一个它的库存交换机stock-event-exchange.

如果想要解锁库存,应该是这样的。

首先订单创建成功之后,库存锁定成功,然后发一个消息给交换机,

这个消息里面的内容有订单编号、仓库编号、哪个商品锁了几个库存,

这个交换机,绑定了两个队列,

一个是按照stock.release.#模糊匹配的路由键绑定的stock.release.stock.queue队列

一个是stock.delay.queue队列

第一次发的库存锁定成功的消息,先使用路由键叫stock.locked

交换机按照这个路由键,找到stock.delay.queue延时队列

延时队列50分钟以后,用stock.release这个路由键,将死信交给库存交换机stock-event-exchange

交换机收到以后,按照这个路由键查找,发现stock.release.#这个模糊匹配的路由键跟它是一样的,然后被交换机路由到我们这个stock.release.stock.queue队列。

接下来的解锁库存服务,专门来处理stock.release.stock.queue里的消息。

最终实现

7.24

在这里插入图片描述

7.23

在这里插入图片描述
具体代码

定时关闭订单

25. 谷粒商城订单系统_第12张图片

首先订单创建成功之后,使用order.create.order路由键将消息路由到order-event-exchange交换机

交换机发现order.create.order这个路由键绑定的是order.delay.queue这个延时队列,然后就把它放到order.delay.queue队列里

过了30分钟,这个延时队列里面的消息,也就是死信,通过order.release.order又路由回order-event-exchange交换机

然后交换机发现这个路由键对应的是order.release.order.queue这个队列,然后就放到order.release.order.queue这个队列里

最终监听order.release.order.queue这个队列的释放订单服务,发现有消息进来了,就会针对里面的数据对其进行关闭订单

问题

这种关闭订单方式会有一些问题

假设订单创建成功之后,订单服务的机器由于卡顿、消息延迟等原因,导致订单未及时取消

此时库存服务的逻辑是订单创建成功之后,它自己会发一个消息,等 40分钟 以后检查之前下单的订单是否已取消,如果是已取消,则解锁库存

结果,库存服务过来查询时,订单服务由于上述原因没有将订单修改为已取消,所以库存就不会解锁,此时的库存消息就算是消费了

等库存服务都检查完了,此时的订单服务才反应过来,然后把订单状态改为已取消了,但是此时库存服务会再有任何的操作了,因为检查订单的消息已经被消费了,库存永远得不到解锁。

解决

为了解决这个问题,我们在监听取消订单的消息时,再发一个消息,主动解锁库存。

主动解锁库存

25. 谷粒商城订单系统_第13张图片

具体是这样的,在释放订单之后,我们主动发一个消息解锁库存,

使用order.release.other将消息路由到交换机,

交换机根据order.release.other.#匹配到stock.release.stock.queue这个队列,并将消息发了过去,

库存服务有对这个队列进行监听,所有一旦有数据来了,就会对其进行解锁库存服务

你可能感兴趣的:(谷粒商城,分布式事务,分布式锁,消息队列,RabbitMQ)