- 订单确认页
- feign远程调用丢失请求头的问题
- feign异步调用丢失请求头的问题
- 原子验证令牌:redis lua脚本
- 下单:订单创建、验证令牌、验价、锁定库存
- 本地事务
- 效果演示
订单业务
页面环境搭建
- 静态资源拷贝进nginx
订单 order 下的 列表、支付、详情、确认订单等页面资源
修改html页面的静态资源引用地址
- 配置 订单服务域名
C:\Windows\System32\drivers\etc\hosts
- 配置网关路由并重启网关服务
- 页面展示
订单中心
- 电商系统涉及到3流,分别是信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
- 订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通
订单构成
- 订单构成图
- 1、用户信息
用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收获信息上的电话。用户可以添加多个收获信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等
- 2、订单基础信息
订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等
+ 订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分
+ 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品时,父子订单就是为后期做拆单准备的
+ 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释
+ 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明
+ 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等
- 3、商品信息
商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量、商品合计价格等
- 4、优惠信息
优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,虚拟币抵扣信息等进行记录
- 5、支付信息
+ 支付流水单号,这个流水单号实在唤起网关支付后支付通道返给电商业务平台的支付流水号,财务通过订单号和流水号与支付通道进行对账使用
+ 支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个--余额支付+第三方支付
+ 商品总金额,每个商品加总后的金额;运费、物流产生的费用;优惠总金额,包括促销活动和优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额之和;实付金额,用户时间需要付款的金额
+ 用户实付金额 = 商品总金额 + 运费 - 优惠总金额
- 6、物流信息
物流信息包括配送方式,物流公司、物流单号、物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点
订单状态
- 1、待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态
- 2、已付款/待发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨、配货、分拣、出库等操作
- 3、待收货/已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
- 4、已完成
用户确认收货后,订单交易完成,后续支付进行结算,如果订单存在问题进入售后状态
- 5、已取消
付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态
- 6、售后中
用户在付款后申请退款,或商家发货后用户申请退换货
售后也同样存在各种状态,当发起售后后生成售后订单,售后订单状态变为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态变更为待退款,退款到用户原账户后订单更新为售后成功
订单流程
- 订单流程是指从订单产生到完成整个流转的过程,从而形成了一套标准流程规则,而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与O2O订单等,所以需要根据不同的类型进行构建订单流程
- 不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成 –> 支付订单 –> 卖家发货 –> 确认收货 –> 交易成功
而每个步骤的背后,订单是如何在多个系统之间交互流转的,可概括如下图
订单创建与支付
- 1、订单创建前需要预览订单,选择收货信息等
- 2、订单创建需要锁定库存,库存有才可以创建,否则不能创建
- 3、订单创建后超时未支付需要解锁库存
- 4、支付成功后,需要进行拆单,根据商品打包方式、所在仓库、物流等进行拆单
- 5、支付的每笔流水都需要记录,以待查账‘
- 6、订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅
逆向流程
- 1、修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息、优惠信息以及其他一些订单可修改范围的内容,此时只需对数据进行变更即可
- 2、订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在支付订单的响应机制上面要做支付的限时处理
幂等性处理
什么是幂等性
- 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣没了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条…这就是没有保证接口的幂等性
哪些情况需要防止
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制
- 其他业务情况
什么情况下需要幂等
- 以SQL为例,有些操作是天然幂等的
1 | sql复制代码SELECT * FROM table WHERE id=?,无论执行多少次都不会改变状态,是天然的幂等 |
幂等解决方案
1、token机制
- 1.1、服务端提供了发送token的接口,我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中
- 1.2、然后调用业务接口请求时,把token携带过去,一般放在请求头部
- 1.3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务
- 1.4、如果token不存在redis中,就表示重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行
- 危险性:
+ 先删除token还是后删除token
- 先删除,可能导致业务确实没有执行,重试还带上之前token,由于防重设计导致,请求还是不能执行
- 后删除,业务处理成功,但是服务闪断,出现超时,没有成功删除token,别人继续重试,导致业务被执行了两次
- 我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求
+ token获取、比较和删除必须是原子性
- `redis.get(token)、token.equals、redis.del(token)`,如果这两个操作不是原子,可能导致,高并发下,都get到同样的数据,判断都成功,继续业务并发执行
- 可以在 redis 中是 lua 脚本完成这个操作
1
kotlin复制代码if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
2、各种锁机制
- 2.1、数据库悲观锁
SELECT * FROM xxxx WHERE id=1 for UPDATE
悲观锁使用时一般伴随着事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦
- 2.2、数据库乐观锁
这种方法适合在更新的场景中
update t_goods set count = count-1,version = version +1 where good_id=2 and version =1
根据 version 版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号,我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2,但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。乐观锁主要使用于处理读多写少的问题
- 2.3、业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁,获取到锁的必须先判断这个数据是否被处理过
3、各种唯一约束
- 3.1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题,但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关
- 3.2、redis set 防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理
4、防重表
- 使用订单号 orderNo 作为去重表的唯一索引,把唯一索引插入进去重表,再进行业务操作,且他们在同一个事务中,这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一个库中,这样就能保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性
- 之前说的 redis 防重也算
5、全局请求唯一id
- 调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过;可以使用nginx设置每一个请求的唯一id;
1 | bash复制代码proxy_set_header X-Request-Id $request_id |
订单业务
订单确认页
- 可以发现订单结算页,包含以下信息:
+ 1、收货人信息:有更多地址、即有多个收货地址,其中有一个默认收货地址
+ 2、支付方式:货到付款、在线支付、不需要后台提供
+ 3、送货清单:配送方式(不做)以及商品列表(根据购物车选中的skuId到数据库中查询)
+ 4、发票:不做
+ 5、优惠:查询用户领取的优惠券(不做)以及可以积分
1 | csharp复制代码# 订单确认页需要用到的数据 |
- 远程查询所有的收货地址
远程查询购物车所有选中的购物项
查询用户积分
Feign远程调用丢失请求头的问题
- feign在远程调用之前要构造请求,调用很多拦截器
RequestInterceptor interceptor:requestInterceptors
Feign异步情况丢失上下文的问题
- 多次远程调用,耗费时间,异步编排优化,这时又会出现请求拦截器中上下文为null的情况
- 异步模式,线程池中多个线程,ThreadLocal 无法跨线程共享数据
线程打印情况
- 解决方法:在进入请求拦截器之前,获取最先的上下文内容,每个异步线程都重新为上下文set一次
订单确认页整体流程
- 流程图
创建订单
下单流程图
- 提交订单流程:验证令牌–创建订单号
原子验证令牌
- 使用redis的方式验证删除令牌,必须保证原子性
可以使用 lua 脚本 确报原子性
+ 未使用 lua 脚本,存在多次验证成功的隐患

+ 使用 lua 脚本,保证原子性

构造订单数据
- 用户提交的订单所需数据
锁定库存
- 锁定库存的逻辑流程图
本文转载自: 掘金