取消订单的优化:使用forupdate行级锁+version

最近随着单量的新增,订单取消导致的并发问题开始出现,我们的取消逻辑是这样的:
1、在发货之前都是可以取消的;
2、针对不同阶段的取消执行不同的业务逻辑;
3、有专门的表记录订单的状态流水:order_status,且和所有的订单状态更改放在一个事务中;
4、通过version来控制订单的更新顺序;

业务简介:

1、订单的正常流程是:
初始化完成 -> 定位中 -> 已定位 -> 拣货下发中 -> 已下发拣货任务 -> 已拣货 -> 已复核 -> 已发货
以上也是订单的正常状态流水。
2、当订单【已定位】且未【已拣货】时,会释放预占,且拣货操作也会释放预占;

项目背景:

项目是分布式系统,订单状态的更改主要以MQ的形式更改,个别是调用服务更新,不同阶段的业务操作也是通过MQ来实现。mysql版本是5.7 ,默认锁级别RR。

问题简述:

最近遇到这么一个情况:订单取消 (上游调用接口取消)和 拣货下发(MQ)并发了,导致订单取消时按照已定位的订单状态执行取消逻辑,即会释放一次预占(给库存发送MQ),但是拣货下发的MQ已经发过去了,而且在消费的时候校验的订单状态是已定位,而非取消,所以消费成功,然后用户操作了拣货,经排查代码,发现拣货操作时没有对订单状态做校验,最终拣货成功,给库存发送了MQ重复释放预占;
预占多释放带来的问题:数据不一致,会影响其他订单的正常流程。

修复方案:

问题是我定位的,和架构一起商讨解决方案(比较影响业务逻辑的修改需要和架构师进行确认),下面是讨论的内容简单整理:
架构:对于这个问题,你的想法是?
我:我想的是在下个流程做拦截,也就是拣货的时候添加状态校验,这样影响业务范围可以最小化。
架构:但是这样修改只能修复当前的这个问题,后续依然会存在类似的问题,我的想法是使用FOR UPDATE,将涉及到的数据行锁住,结合version字段,来处理这个问题。

修复过程:

1、明确for update 的特性和用法
2、在合适的业务代码添加for update

1、明确for update的特性和用法
for update可以实现 InnoDB的 行级锁,
需要注意的是,for update只能和select 使用,且只有当走的索引是唯一索引时才会锁行,否则锁表,为了以防万一,建议使用主键作为where条件,这样就能确保走的是唯一索引;
另外还需要注意的是,for update 需要和事务一起使用,这样通过for update 获取最新的数据,并锁住行,然后执行更新操作,事务结束释放锁,别的业务场景才能执行更新。
2、在合适的业务代码添加for update
在订单取消的处理逻辑中,先添加事务注解@Transactional(默认即可,可重复读),再根据入参的业务字段查询具体的订单主档信息orderM,然后再通过 FOR UPDATE + 订单主档的主键orderM.getId() 查询出最新数据并锁住该行数据(MVCC 有说明为什么使用for update 可以获取最新的数据),然后执行更新操作,最后释放锁。

分析为何使用FOR UPDATE+version 可以解决上面的问题:
假设已定位时的订单信息orderM的version是5,status是20,当订单取消和拣货任务下发并发时,订单取消通过for update获取到最新的订单信息version=5,status=20,并锁住,然后执行取消逻辑,更新订单信息(取消订单更新订单状态不需要version的校验,以此实现任何时候都能取消并更新订单状态)为version = 6,status = 404,在事务为提交之前,拣货任务下发中的业务逻辑也在执行,它获取到的订单信息也是 version=5,status=20,根据这些信息执行对应的业务逻辑,最后更新订单(where version = 5)的时候会 等待锁释放才能更新,当订单取消更新订单信息提交释放锁后,拣货任务下发中更新订单的SQL就会执行失败,因为version已经变成了6,不再是5,所以失败,抛出异常,数据回滚,而取消正常执行。
那么当拣货下发中先执行订单信息的更新,此时订单取消了,然后通过FOR UPDATE获取最新数据时会等待拣货任务下发更新订单信息的事务提交,最终获取最新数据,然后再执行取消逻辑。

代码实现 (只保留该问题涉及到的代码)

/**
     * 接收订单取消
     *
     * @param outerOrderCancelDto
     * @return
     */
    @Override
    @LoggerTag(position = LogPositionEnum.ALL, name = "argument")
    @Transactional
    public Result acceptOrderCancel(OuterOrderCancelDto outerOrderCancelDto)</
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值