使用延迟队列解决分布式事务问题——以订单未支付过期,解锁库存为例

目录

一、前言

二、库存

三、订单


一、前言

上一篇使用springcloud-seata解决分布式事务问题-2PC模式我们说到了使用springcloud-seata解决分布式的缺点——不适用于高并发场景

因此我们使用延迟队列来解决分布式事务问题,即使用柔性事务-可靠消息-最终一致性方案(异步确保型)

以下是下订单的代码

//    @GlobalTransactional  不使用seata
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        response.setCode(0);
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // TODO 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // TODO 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                // TODO 3、保存订单
                saveOrder(order);

                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> collect = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                // TODO  4、锁库存
                // 出异常后,因为远程锁库存成功,但是忘了原因超时了,订单回滚,库存不回滚

                // 为了保证高并发,库存服务自己要回滚,可以发消息给库存服务
                // 库存服务本身也可以使用自动解锁模式 即使用消息队列
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if(r.getCode() == 0) {
                    // 锁成功
                    response.setOrder(order.getOrder());

                    // TODO 5 出异常
//                    int i = 10/0;
                    return response;
                } else {
                    // 锁定失败
                    // 抛异常才能使事务回滚
                    response.setCode(3);
                    throw new NoStockException((String)r.get("msg"));

//                    return response;
                }
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }

    }

二、库存

 库存服务设计图,首先创建stock-event-exchange交换机,还有stock.release.stock.queuestock.delay.queue(死信队列)两个队列,交换机和stock.release.stock.queue之间通过路由stock.release.#绑定,交换机和stock.delay.queue(死信队列)通过路由stock.locked绑定

流程解释:当库存锁定成功后,发消息给交换机,交换机通过路由发送到死信队列中,通过死信队列的延迟效果,在时间到期后再路由到交换机,交换机再放入普通队列(绿色),此时只要有方法监听这个队列,就可以拿到消息进行消费

向rabbitmq注册队列、交换机和绑定的代码如下:

@Configuration
public class MyRabbitConfig {


    @Autowired
    RabbitTemplate template;

    /*
     * 使用JSON序列化机制,进行消息转换
     */
    @Bean
    public MessageConverter messageConverter() {


        return new Jackson2JsonMessageConverter();
    }

//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void handle(Message message) {
//
//    }

    @Bean
    public Exchange stockEventExchange() {
        // String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new TopicExchange("stock-even-exchange", true, false, null);
    }

    @Bean
    public Queue stockReleaseStockQueue() {
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        return new Queue("stock.release.stock.queue",true, false,false, null );
    }

    @Bean
    public Queue stockDelayQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-even-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        arguments.put("x-message-ttl", 120000); // 消息过期时间 1分钟
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Binding stockLockBinding() {

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

    @Bean
    public Binding stockReleaseBinding() {

        return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE,
                "stock-even-exchange",
                "stock.locked",null);
    }


}

库存锁定方法,若锁定成功会发送消息到死信队列

    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {

        // 先创建订单详情表
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());
        orderTaskService.save(taskEntity);
        // 1、找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();

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

        // 2、锁定库存

        // 1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
        // 2、锁定失败,前面保存的工作单信息就回滚了。即使要解锁记录,由于去数据库查不到id,所以就不用解锁了

        for (SkuWareHasStock hasStock : collect) {
            Boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareId = hasStock.getWareId();
            if(wareId == null || wareId.size() == 0) {
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            for(Long ware : wareId) {
                // 返回受影响的行数 成功返回1 失败返回0
                Long count = wareSkuDao.lockSkuStock(skuId, ware, hasStock.getNum());
                if(count == 1) {
                    //成功
                    skuStocked = true;
                    // TODO 告诉MQ库存锁定成功
                    WareOrderTaskDetailEntity orderTaskDetailEntity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), ware, 1);
                    orderTaskDetailService.save(orderTaskDetailEntity);
                    // 通知rabbitmq锁定成功
                    StockLockedTo stockLockedTo = new StockLockedTo();
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(orderTaskDetailEntity, stockDetailTo);
                    // 只发id不行,防止回滚以后详情表也被回滚了,而库存又被扣减了,此时就无法解锁了
                    stockLockedTo.setDetail(stockDetailTo);
                    stockLockedTo.setId(taskEntity.getId());
                    rabbitTemplate.convertAndSend("stock-even-exchange", "stock.locked", stockLockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if(skuStocked == false) {
                throw new NoStockException(skuId);
            }
        }
        return true;
    }

然后是监听普通队列的方法

@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁
     *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     *
     *  2、订单失败
     *      库存锁定失败
     *
     *   只要解锁库存的消息失败,一定要告诉服务解锁失败
     *
     *   该方法是处理库存自己发给自己的
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******收到解锁库存的信息******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}

wareSkuService的unlockStock方法如下,只有订单为取消状态或者订单不存在才解锁库存

    /*
     * 解锁的方法,如果报错就抛异常让StockReleaseListener去抓
     */
    @Override
    public void unlockStock(StockLockedTo to) {
        //库存工作单的id
        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();

        /**
         * 解锁
         * 1、查询数据库关于这个订单锁定库存信息
         *   有:证明库存锁定成功了
         *      解锁:订单状况
         *          1、没有这个订单,必须解锁库存
         *          2、有这个订单,不一定解锁库存
         *              订单状态:已取消:解锁库存
         *                      已支付:不能解锁库存
         */
        WareOrderTaskDetailEntity taskDetailInfo = orderTaskDetailService.getById(detailId);
        if (taskDetailInfo != null) {
            //查出wms_ware_order_task工作单的信息
            Long id = to.getId();
            WareOrderTaskEntity orderTaskInfo = orderTaskService.getById(id);
            //获取订单号查询订单状态
            String orderSn = orderTaskInfo.getOrderSn();
            //远程查询订单信息
            R orderData = orderFeignService.getOrderStatus(orderSn);
            if (orderData.getCode() == 0) {
                //订单数据返回成功
                OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});

                //判断订单状态是否已取消或者支付或者订单不存在
                if (orderInfo == null || orderInfo.getStatus() == 4) {
                    //订单已被取消,才能解锁库存
                    if (taskDetailInfo.getLockStatus() == 1) {
                        //当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放在队列里面,让别人继续消费解锁
                //远程调用服务失败
                throw new RuntimeException("远程调用服务失败");
            }
        } else {
            //无需解锁
        }
    }

unLockStock方法如下:

    public void unLockStock(Long skuId, Long wareId, Integer num ,Long taskDetailId) {
        wareSkuDao.unLockStock(skuId, wareId, num);
        // 解锁后应该改变订单详情的状态
        WareOrderTaskDetailEntity wareOrderTaskDetailEntity = new WareOrderTaskDetailEntity();
        wareOrderTaskDetailEntity.setId(taskDetailId);
        wareOrderTaskDetailEntity.setLockStatus(2);
        orderTaskDetailService.updateById(wareOrderTaskDetailEntity);
    }

小结:即锁库存成功后,我们要搞一个定时任务一样,在一段时间(当然这里设计的时间需要比订单支付的时间长,比如pdd下单,没支付,30分钟后就会自动取消订单,所以要比30分钟长才可以)后检查一下订单是否支付,没有支付过期了或者说取消订单了,我们需要把锁住的库存恢复这里恢复我们使用得是wms_ware_order_task表(主要记录订单,即是哪一个订单的)wms_ware_order_task_detail表(记录订单中的每一项商品,比如skuId为42的锁住了2件)

其结构如下:

接着我们需要考虑传递什么值到rabbitmq队列里,使得拿到值的方法可以有足够的信息恢复(解锁库存),因此我们封装了一个StockLockedTo,其中的StockDetailTo其实就是表ware_order_task的实体,只是为了传输重新封装了一个StockDetailTo,所以监听的方法拿到消息中的实体后就可以进行解锁库存操作了 

@Data
public class StockLockedTo {

    private Long id; //库存工作单的id
    private StockDetailTo detail; // 工作详情
}

 三、订单

那么如果订单被取消的时候直接发送消息给mq的库存队列,然后mq进行解锁,是不是一个双重保障呢?确实如此,就相当于订单是主体,而库存的队列相当于补偿

订单设计图如下

解读:创建一个order-event-exchange交换机和两个队列,死信队列order.delay.queue和普通队列order.release.order.queue,交换机和order.delaly.queue通过路由order.create.order绑定,交换机和order.release.order.queue通过路由order.release.order绑定。当订单创建成功后,发消息到死信队列,如果30分钟内没付款,就会通过路由器进入普通队列,此时监听普通队列的方法就可以取出消息更改订单状态,同时发送解锁消息给库存的队列

向rabbitmq注册队列和交换机的代码:
 

@Configuration
public class MyMQConfig {
    // 通过bean的形式向rabbitmq创建Queue、Exchange、Binding



    /**
     * 死信队列
     *
     * @return
     */@Bean
    public Queue orderDelayQueue() {
        /*
            Queue(String name,  队列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自动删除
            Map<String, Object> arguments) 属性
         */
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "order-event-exchange");
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);

        return queue;
    }

    /**
     * 普通队列
     *
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {

        Queue queue = new Queue("order.release.order.queue", true, false, false);

        return queue;
    }

    /**
     * TopicExchange
     *
     * @return
     */
    @Bean
    public Exchange orderEventExchange() {
        /*
         *   String name,
         *   boolean durable,
         *   boolean autoDelete,
         *   Map<String, Object> arguments
         * */
        return new TopicExchange("order-event-exchange", true, false);

    }


    @Bean
    public Binding orderCreateBinding() {
        /*
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map<String, Object> arguments
         * */
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseBinding() {

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

    /*
     * 订单释放直接发送消息到进行绑定
     */
    @Bean
    public Binding orderReleaseOtherBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
}

下订单的方法

    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        response.setCode(0);
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // TODO 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // TODO 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                // TODO 3、保存订单
                saveOrder(order);

                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> collect = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                // TODO  4、锁库存
                // 出异常后,因为远程锁库存成功,但是忘了原因超时了,订单回滚,库存不回滚

                // 为了保证高并发,库存服务自己要回滚,可以发消息给库存服务
                // 库存服务本身也可以使用自动解锁模式 即使用消息队列
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if(r.getCode() == 0) {
                    // 锁成功
                    response.setOrder(order.getOrder());

                    // TODO 5 出异常
//                    int i = 10/0;
                    // TODO 订单创建成功发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
                    return response;
                } else {
                    // 锁定失败
                    // 抛异常才能使事务回滚
                    response.setCode(3);
                    throw new NoStockException((String)r.get("msg"));

//                    return response;
                }
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }
    }

监听普通队列的方法

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
    @Autowired
    OrderService orderService;
    @RabbitHandler
    public void lisentner(OrderEntity entity, Channel channel, Message message) throws IOException {

        try {
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
        System.out.println("收到订单信息,即将关闭订单" + entity);

    }


}

closeOrder方法如下:

    @Override
    public void closeOrder(OrderEntity entity) {
        OrderEntity orderEntity = this.getById(entity.getId());
        // 订单的状态需要是新创建的
        if(orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
            //不能使用上面的orderEntity,然后直接更新状态进行更新,因为在创建过程中经历了那么多
            // 步骤,可能一些属性已经发生改变了。
            OrderEntity update = new OrderEntity();
            update.setId(orderEntity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity, orderTo);
            // 立即发送消息给库存通知其解锁
            rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
        }
    }

因为封装到rabbitmq进行传递的消息不一样,所以库存的监听方法需要增加

@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁
     *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     *
     *  2、订单失败
     *      库存锁定失败
     *
     *   只要解锁库存的消息失败,一定要告诉服务解锁失败
     *
     *   该方法是处理库存自己发给自己的
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******收到解锁库存的信息******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /*
     * 该方法是库存处理订单服务发消息队列的消息
     * 为什么会有这个步骤
     * 1、当订单服务如果卡顿,然后还没取消订单
     * 2、此时库存消息队列里的时间到了,库存监听到然后一看订单不是取消状态,所以直接更改库存的详情状态
     * 3、而过后订单服务反应过来了,取消了订单,但库存永远也回不去了,被锁死了
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
        log.info("******订单关闭准备解锁库存******");
        try {

            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}
unlockStock方法如下:
    /*
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期,查订单状态新建状态,什么都不做就走了
     * 导致卡顿的订单永远不能解锁库存
     */
    @Override
    public void unlockStock(OrderTo to) {
        String orderSn = to.getOrderSn();
        WareOrderTaskEntity taskEntity = orderTaskService.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", taskEntity.getId())
                .eq("lock_status", 1));

        for (WareOrderTaskDetailEntity entity : entities) {
            unLockStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum(), entity.getId());
        }

    }

评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zoeil

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值