谷粒商城下单的事务控制与消息队列

本文介绍了如何使用RabbitMQ的延迟队列和死信队列实现分布式事务控制,确保在下单过程中库存服务和订单服务的最终一致性。通过TTL机制和解锁订单逻辑,解决了并发问题和异常处理中的库存释放问题。

一、由于下单涉及到库存服务和订单服务,为了综合分布式事务控制和并发问题,选用柔性事务-可靠消息-最终一致性(异步确保型)控制下订单的分布式事务。具体采用 RabbitMQ延迟队列(消息队列TTL+死信队列)实现。

分布式事务控制:

当提交订单时,当库存已经锁定的后续业务执行异常,本地事务控制只能回滚对订单服务连接的数据库相应的操作,而库存服务无法感知到异常,那么锁定的库存就永远无法释放。

而选取RabbitMQ对事物控制的逻辑如下:当库存服务成功锁定库存时,会给延迟队列stock.delay.queue发送消息,这个队列会设置一个TTL(消息在队列中的存活时间),这个队列不会被别人监听,当TTL结束时,消息中的队队列不会乱丢,他会将过期的消息发送到死信队列 stock.release.stock.queue中,用户只需要监听这个队列,消费队列中的消息,消费逻辑如下:

// 解锁
// 查询数据库 关于这个订单的锁库存信息
//   有:证明库存锁定成功
//      解锁:订单情况
//           没有订单:说明订单逻辑异常回滚了,那么库存也应该回滚
//           有订单:说明订单逻辑也成功了
//              订单状态:
//                  已取消:解锁库存                
//                  没取消:不能解锁
//   没有:库存锁定失败 库存事务回滚,无需解锁
public void unLockStock(StockLockedTo to) {
        System.out.println("收到解锁库存的消息....");
        StockDetailTo detailTo = to.getDetailTo();
        // 只要解锁库存的消息失败 一定要告诉服务器解锁失败
        WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId()); // 查询数据库 关于这个订单的锁库存信息
        if(detailEntity!=null){
            // 远程查询订单情况
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(to.getId());
            R r = orderFeignService.getOrderByOrderSn(taskEntity.getOrderSn()); // 订单信息
            if(r.getCode() == 0) { // 成功调用
                OrderVo data = r.getData(new TypeReference<OrderVo>() {
                });
                if (data == null || data.getStatus() == 4) { //订单被取消
                    // 解锁库存
                    if(detailEntity.getLockStatus() == 1){ // 工作单状态是 已锁定 才会解锁
                        unLockStock(detailEntity.getSkuId(), detailEntity.getWareId(), detailEntity.getSkuNum(), detailEntity.getId());
                    }
                }
            }else{
                throw new RuntimeException("远程服务失败");
            }
        }
    }

由此来控制下单逻辑中库存服务和订单服务的事务。同时也能对已经取消的订单进行库存解锁。

二、考虑一种业务逻辑,用户下单后,如果一直不去支付,那么系统就应该在指定的时间内取消订单,因此再次使用RabbitMQ,下单成功后,会自动给队列order-delay-queue发送下单成功消息,该队列同样不被任何用户监听,当这个延迟队列的消息TTL结束后,会将过期的消息保存到死信队列order.release.order.queue中。用户只需要监听这个队列,消息处理逻辑如下。

    public void closeOrder(OrderEntity orderEntity) {
        // 查询订单最新状态
        OrderEntity order = this.getById(orderEntity.getId());
        if(order.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){ // 待付款状态才关单
            OrderEntity updateEntity = new OrderEntity();
            updateEntity.setId(orderEntity.getId());
            updateEntity.setStatus(OrderStatusEnum.CANCLED.getCode()); //设置状态为取消状态
            this.updateById(updateEntity);
            // MQ发送一个订单释放的消息
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(order,orderTo);
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }

为什么上面修改了订单状态后还要个MQ发送一个订单释放的消息呢?

我们之前成功创建订单时肯定给队列stock.delay.queue发送过消息了,只需要将stock.delay.queue这个队列的TTL时间比order.release.order.queue时间长就可以了,order.release.order.queue的消息应该先被消费了,订单状态被修改为已取消状态了,此后消费stock.delay.queue消息时,会发现订但状态时取消状态,他就会去解锁库存。

但是考虑一种问题:订单创建成功给MQ发送消息时由于网络原因,很久都没有发送过去,而库存锁定成功的消息却已经发送过去,这就可能导致stock.delay.queue里面的消息会先一步被消费,此时查询订单转状态为新建状态,他就不会去释放内存,而后面order.release.order.queue里面的消息在被消费时,订单状态虽然变成已取消,但是库存也就永远无法释放了。

因此我们在订单超时消息处理结束后给MQ的stock.release.stock.queue队列在发送一个消息,表示某个订单已经释放,监听stock.release.stock.queue这个队里的客户端再去消费这个消息,主动去解锁库存。后面即使stock.delay.queue的TTl到期,他也会发现锁库存工作单的状态是已解锁了,不会再去解锁。逻辑如下:

    public void unLockStock(OrderTo to) {
        String orderSn = to.getOrderSn();
        // 解放库存
        // 查询最新的库存解锁状态 防止重复解锁库存
        WareOrderTaskEntity taskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        // 按照工作单 找到所有没有解锁的库存
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(
                new QueryWrapper<WareOrderTaskDetailEntity>()
                        .eq("task_id", taskEntity.getId())
                        .eq("lock_status", 1));
        for (WareOrderTaskDetailEntity orderTaskDetailEntity : list) {
            unLockStock(orderTaskDetailEntity.getSkuId(),orderTaskDetailEntity.getWareId(),orderTaskDetailEntity.getSkuNum(),orderTaskDetailEntity.getTaskId());
        }

    }

    @Transactional
    public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId){
        this.baseMapper.unLockStock(skuId,wareId,num);
        // 更新 库存工作单状态 为已解锁
        WareOrderTaskDetailEntity orderTaskDetailEntity = new WareOrderTaskDetailEntity();
        orderTaskDetailEntity.setId(taskDetailId);
        orderTaskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(orderTaskDetailEntity);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值