基于RabbitMQ实现下单减库存的最终一致性分布式事务

本文详细描述了一个使用RabbitMQ的死信队列机制来处理订单创建、库存锁定、过期取消和自动解锁的过程,确保分布式系统的可靠性和最终一致性,同时提及了消息丢失问题及其解决方案的预告。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

十一假期真是让我休息了一段时间,更新的进度都慢了。现在状态终于又调整回来了,继续更新。

话不多说, 先放张总数据流向图。

文字解读

第一步:用户下单调用订单服务,我们直接把这个消息发给MQ的订单交换机,同时调用远程库存服务锁定库存。

第二步:发给订单交换机的消息自动转发给死信队列,死信队列经过30分钟后过期会自动转发给我们另一个普通的队列。

第三步:跟第二步的同时,调用了库存服务的锁库存方法后,库存服务也会给库存交换机发一条消息,交换机转发给死信队列,经过60分钟后过期再转发给另一个普通队列。

第四步:这时候就是我们的普通队列监听器处理业务的时候了,订单普通队列监听器监听到消息后,就说明这个消息是已经经过了30分钟后到达的,那也就是过期的订单需要删除。所以执行业务逻辑把这个订单状态改为已取消。业务逻辑处理完成后,他还要给库存交换机发一条消息,让库存服务去解锁刚才锁定的库存。

第五步:库存服务中的普通队列监听器,监听到第四步中的订单服务发来的解锁库存消息后,就要去解锁库存。

第六步:这一步可以说是库存的自动解锁一可以说是给系统做二次解锁保障。其实上面五步已经完成了下单锁库存,取消订单解库存。但是上面的步骤中任何一步出现问题都有可能导致库存解锁失败。所有这一步就是更保障了系统的可靠性。我们设置库存的死信队列是60分钟(必须大于订单的过期时间)就是为了,如果上面任何一个步骤出现问题后, 我们还在60分钟后,扫描每一个我们已经锁定库存的记录,重新判断该记录的订单状态,来判断我们要不要再解锁库存。

整个流程的核心是RabbitMQ的死信队列

        主要用到的就是RabbitMQ组件,使用RabbitMQ的死信队列,来完成我们临时订单的定时过期功能,并一并实现库存锁定功能和库存自动解锁功能。

 RabbitMQ的死信队列其实就是一个普通队列带有特定参数后就成了我们所说的死信队列。如下:

 @Bean
    public Queue orderDelayQueue() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange","order-event-exchange");
        args.put("x-dead-letter-routing-key", "order.release.order");
        args.put("x-message-ttl", 60000);
        return new Queue("order.delay.queue",true,false,false,args);
    }

如果还有小伙伴不熟悉RabbitMQ的基本构成和基本使用的同学,可以先查看我以前的文章,有关于RabbitMQ的详细的介绍。http://t.csdnimg.cn/kdgzw

接下来用代码来记录下完整的流程

第一步、交换机和队列的创建配置

1、订单服务的MQ配置


@Configuration
public class MyRabbitMQConfig {
    /* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */

    /**
     * 死信队列
     * @return
     */
    @Bean
    public Queue orderDelayQueue() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange","order-event-exchange");
        args.put("x-dead-letter-routing-key", "order.release.order");
        args.put("x-message-ttl", 1800000);
        return new Queue("order.delay.queue",true,false,false,args);
    }

    /**
     * 普通队列
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {
        return new Queue("order.release.order.queue",true,false,false);
    }

    /**
     * topic exchange
     * @return
     */
    @Bean
    public Exchange orderEventExchange() {
        return new TopicExchange("order-event-exchange", true, false);
    }

    /**
     * 死信 绑定关系1
     * @return
     */
    @Bean
    public Binding orderCreateBinding() {
        return new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
    }

    /**
     * 正常队列绑定关系2
     * @return
     */
    @Bean
    public Binding orderReleaseBinding() {
        return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order", null);
    }

    /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOtherBinding() {

        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }


}

2、库存服务的MQ配置


@Configuration
public class MyRabbitMQConfig {
    

    @Bean
    public Exchange stockEventExchange() {
        return new TopicExchange("stock-event-exchange", true, false);
    }

    /**
     * 普通队列
     * @return
     */
    @Bean
    public Queue stockReleaseStockQueue() {
        return new Queue("stock.release.stock.queue",true,false,false);
    }


    /**
     * 延迟队列
     * @return
     */
    @Bean
    public Queue stockDelay() {

        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        arguments.put("x-message-ttl", 3600000);

        Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
        return queue;
    }

    /**
     * 交换机与普通队列绑定
     * @return
     */
    @Bean
    public Binding stockLocked() {
        Binding binding = new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);

        return binding;
    }


    /**
     * 交换机与延迟队列绑定
     * @return
     */
    @Bean
    public Binding stockLockedBinding() {
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }


}

第二步、下单服务

创建完订单后,就去调用远程的库存服务进行锁库存,同时向MQ的订单交换机发送了一条消息。 

// 本地事务不满足了
    // 。 要使用分布式事务
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) throws Exception {

          /**
         *  这里我删除了部分我自己的业务逻辑,所以这个代码不能直接用,
         *  所以要适当的修改,下面才是主要的下单库存逻辑,仔细研究下面的代码就可以了
         */
            

        // 保存订单
        saveOrder(order);

        // 库存 处理 , 重要地方 , 只要锁定库存位置出现异常 , 则回滚
        WareSkuLockVo lockVo = new WareSkuLockVo();
        lockVo.setOrderSn(order.getOrder().getOrderSn());

        // 要锁定的商品
        List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map(item -> {
            OrderItemVo orderItemVo = new OrderItemVo();
            orderItemVo.setSkuId(item.getSkuId());
            orderItemVo.setCount(item.getSkuQuantity());
            orderItemVo.setTitle(item.getSkuName());
            return orderItemVo;
        }).collect(Collectors.toList());
        lockVo.setLocks(orderItemVos);

        /**
         *  重点 :::!!!!!!
         *      使用RabbitMQ 实现分布式事务的最终一致性 , 并发性能更好
         *       使用RabbitMQ 实现分布式事务的最终一致性 , 并发性能更好
         *        使用RabbitMQ 实现分布式事务的最终一致性 , 并发性能更好
         */
        // todo  远程调用  锁库存
        try {
            R r = wmsFeignService.orderLockStock(lockVo);
            if (r.getCode() == 0) {
                // 锁定成功
                responseVo.setOrder(order.getOrder());

                // todo 发消息给订单交换机,  定时去关闭
                String uuid = UUID.randomUUID().toString().replace("-", "");
                order.getOrder().setMqMessageId(uuid);
                CorrelationData correlationData = new CorrelationData(uuid);


                rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder(), correlationData);


                return responseVo;
            } else {
                String msg = (String) r.get("msg");
                throw new NoStockException(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
            responseVo.setCode(3);
            throw new NoStockException("远程库存未响应");
        }
        return responseVo;
    }

 第三步、库存服务锁定库存

 锁定库存,并且发送一条消息给 MQ。

 @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean orderLockStock(WareSkuLockVo vo) {
        WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
        wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
        wareOrderTaskEntity.setCreateTime(new Date());
        wareOrderTaskService.save(wareOrderTaskEntity);

        // 找库存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
            skuWareHasStock.setSkuId(item.getSkuId());
            skuWareHasStock.setNum(item.getCount());
            //查询这个商品在哪个仓库有库存
            List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(item.getSkuId());
            skuWareHasStock.setWareId(wareIdList);
            return skuWareHasStock;
        }).collect(Collectors.toList());

        // 锁库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (CollectionUtils.isEmpty(wareIds)) {
                throw new NoStockException(skuId);
            }

            for (Long wareId:wareIds) {
                //锁定成功就返回1,失败就返回0   锁定库存,不是真实的减库存
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1){
                    // 锁定成功 , 真实减库存
                    skuStocked = true;
                    //

                    WareOrderTaskDetailEntity taskDetail = new WareOrderTaskDetailEntity();
                    taskDetail.setSkuId(skuId);
                    taskDetail.setSkuName("");
                    taskDetail.setTaskId(wareOrderTaskEntity.getId());
                    taskDetail.setWareId(wareId);
                    taskDetail.setLockStatus(1);
                    taskDetail.setSkuNum(hasStock.getNum());
                    wareOrderTaskDetailService.save(taskDetail);

                    // MQ 发消息锁定库存
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(wareOrderTaskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(taskDetail, stockDetailTo);
                    lockedTo.setDetail(stockDetailTo);
                    String uuid = UUID.randomUUID().toString().replace("-", "");
                    lockedTo.setMqMessageId(uuid);
                    CorrelationData correlationData = new CorrelationData(uuid);  //  给回调函数 使用

                    // 发送消息给库存交换机, 给延时队列, 说 我要锁定库存。
                    rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo, correlationData);


                    break;
                }
            }
            if (!skuStocked) {
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }

        //3、肯定全部都是锁定成功的
        return true;
    }

第四步、订单过期MQ监听器

 订单过期,取消订单,最后再给MQ库存交换机发消息,告诉库存要解锁库存。

 @RabbitListener(queues = "order.release.order.queue")
    @RabbitHandler
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        System.out.println("过期订单, 准备关闭。。。");
        try {
            orderService.closeOrder(orderEntity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        }catch (Exception e){
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
@Override
    public void closeOrder(OrderEntity orderEntity) {

        OrderEntity orderSn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderEntity.getOrderSn()));
        if (orderSn != null && orderSn.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
            // 幂等性, 只有待付款订单 才删除
            OrderEntity update = new OrderEntity();
            update.setId(orderEntity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);

            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderSn, orderTo);
            // 发消息给mq  , 去解锁库存
            try {

                // todo  每个发消息的地方 都要做好 日志表的记录 , 防止消息丢失
                String uuid = UUID.randomUUID().toString().replace("-", "");
                orderTo.setMqMessageId(uuid);
                CorrelationData correlationData = new CorrelationData(uuid);

                rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo, correlationData);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

第五步、库存服务监听器

两个监听方法,一个监听订单服务发来的消息,一个监听库存锁定自动到期的消息。


/**
 * 自动解锁库存
 */
@Service
@RabbitListener(queues = "stock.release.stock.queue")
@Slf4j
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    @Autowired
    MqMessageService mqMessageService;


    /**
     * 1、库存自动解锁
     *      下单成功,库存锁定成功,后续业务失败, 导致订单回滚, 这时 库存就要解锁
     * 2、订单失败
     *      库存锁定失败
     *      只要解锁库存的消息失败,一定要告诉服务解锁失败
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******时间到,自动解锁,收到解锁库存的信息******");
        try {
            // 解锁 库存
            wareSkuService.unLockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        }catch (Exception e) {
            e.printStackTrace();
            // 解锁失败  , 消息重新放回 队列
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }



    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Channel channel, Message message) throws IOException {
        log.info("订单取消成功, 开始解锁库存。。。");
        try{
            wareSkuService.unLockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        }catch (Exception e){
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

}

 库存消息时间到期,自动解锁库存的业务逻辑

@Override
    public void unLockStock(StockLockedTo to) {
        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();

        /**
         * 解锁
         *      只解锁自己的库存业务相关的  锁定, 也就是库存订单任务表和库存订单任务详情表 。
         *      所以 要连个订单服务, 去查这个订单的具体状态。
         *      1、订单存在并且状态未取消
         *      2、订单不存在
         *      这两种都要 ,解锁
         */

        // 首选  先看自己库存 有没有加锁成功, 生成记录
        WareOrderTaskDetailEntity taskDetail = wareOrderTaskDetailService.getById(detailId);
        if (taskDetail != null) {
            // 查工作单 信息
            Long taskId = to.getId();
            WareOrderTaskEntity orderTask = wareOrderTaskService.getById(taskId);
            String orderSn = orderTask.getOrderSn();
            // todo  远程查看订单服务 找order  , 看是否 订单成功
            R orderStatus = orderFeignService.getOrderStatus(orderSn);
            if (orderStatus.getCode() == 0) {
                // 成功
                OrderVo orderInfo = (OrderVo) orderStatus.getData("data", new TypeReference<OrderVo>() {
                });
                // 下面两种状态 都要解锁
                if (orderInfo == null ){  // 没有订单,说明下订单失败
                    if (taskDetail.getLockStatus() == 1) {  // 库存订单任务  状态为 已锁定 , 才能解锁  。
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }else if (orderInfo.getStatus() == 4){  // 已关闭
                    if (taskDetail.getLockStatus() == 1) {  // 库存订单任务  状态为 已锁定 , 才能解锁  。
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }

            }else {
                // 远程失败
                throw new RuntimeException("远程服务调用失败。");
            }
        }
    }

订单取消消息的业务处理逻辑

 @Transactional
    @Override
    public void unLockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        List<WareOrderTaskDetailEntity> taskDetailEntityList = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", orderTaskEntity.getId()).eq("lock_status",1));

        for (WareOrderTaskDetailEntity item : taskDetailEntityList) {
            unLockStock(item.getSkuId(),item.getWareId(), item.getSkuNum(), item.getId());
        }
    }

总结,这一套下来就实现了,基于RabbitMQ的分布式最终一致性。

但是,在这一套流程中还存在着一些问题。 就是在每个服务之间调用,向MQ发送消息接受消息的过程中可能因为种种原因很容易出现消息丢失的问题。这也是一个非常严重的问题。

在下一篇文章,我将再给大家介绍一下RabbitMQ消息丢失问题的处理方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值