订单服务的分布式事务-延迟队列

1.需求:高并发下,远程分布式事务seata不合适。如订单服务

1.1 解决方法-延迟队列

高并发下,分布式事务Seata方案不适合。
方案:可靠消息 + 最终一致性方案(异步确保型)
可以使用延迟队列,实现远程服务的事务,完成数据最终一致性。

1.2 延迟队列的介绍

1.2.1 延迟队列的场景-定时任务

在这里插入图片描述

1). 定时任务的时效性问题

在这里插入图片描述

2). 定时任务的时效性问题的解决–RabbitMQ延时队列(实现定时任务)

1.2.2 延迟队列的场景-RabbitMQ延时队列(实现定时任务)

场景:
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案:
spring的 schedule 定时任务轮询数据库
缺点:
消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决: rabbitmq的消息TTL和死信Exchange结合

1).消息的TTL(Time To Live)-消息的存活时间

• 消息的TTL就是消息的存活时间
• RabbitMQ可以对队列消息分别设置TTL。
…• 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的
设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
…• 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队
列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的
TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x- message-ttl属性来设置时间,两者是一样的效果。

2).Dead Letter Exchanges(DLX)-死信路由

• 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,
一个路由可以对应很多队列。(什么是死信,是没有的消息)
…• 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。
也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false
…• 上面的消息的TTL到了,消息过期了。
…• 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
• Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。
只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
• 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机。
结合二者,其实就可以实现一个延时队列
• 手动ack&异常消息统一放在一个队列处理,建议的两种方式
…• catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费
…• 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败

3).延时队列实现-1(推荐)

在这里插入图片描述
推荐 延时队列1:给队列设置过期时间。因为RabbitMQ采用惰性检查机制,

4).延时队列实现-2

在这里插入图片描述

1.2.3 延迟队列的模拟

使用延迟队列的实现方式:给队列加过期时间

1).延迟队列-简单模式

在这里插入图片描述

2个交换机、2个队列、2个绑定

2).延迟队列-升级模式(推荐)

在这里插入图片描述
一个交换机、2个队列、2个绑定。(共享一个交换机)

3).测试代码-order项目
a.RabbitMQ的配置类
@Configuration
public class MyRabbitMQConfig {
    /**
     * 容器中的2个Queue、1个Exchange、2个Binding。 会自动创建(在RabbitMQ)不存在的情况下
     * RabbitMQ只要有, @Bean声明属性不会发生变化也不会覆盖
     * 采用:1个交换机、2个队列、2个绑定的模式
     * 原理:订单创建成功,先把消息放入延迟队列(死信队列)。如果过期时间到,把消息放入订单队列,等待消费者释放消息。
     */

    @RabbitListener(queues = "order.release.order.queue")
    public void listener(OrderEntity order, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息:准备关闭订单 "+ order.getOrderSn());
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不批量保存
    }

    //死信队列(延迟队列)-存放原始的消息
    @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;
    }

    //普通队列(存放死信的队列)
    @Bean
    public Queue orderReleaseQueue() {
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }

    //TopicExchange。交换机
    @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);
    }
}
b.测试发送消息的接口
@Controller
public class HelloController {
    @Autowired
    RabbitTemplate rabbitTemplate;

    //测试RabbitMQ的发送消息
    @GetMapping("/test/createOrder")
    @ResponseBody
    public String createOrderTest( ){
        //订单下单成功
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(UUID.randomUUID().toString());
        orderEntity.setModifyTime(new Date());
        //给MQ发送消息
        rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
        return "ok";
    }
 }

注意添加amq依赖,spring-boot-starter-amqp

1.3 订单延迟队列-订单关单

在这里插入图片描述

加amq依赖:
spring-boot-starter-amqp
添加RabbitMQ配置:
配置消息队列MQ
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/
启动类:开启rabbitMQ @EnableRabbit

1.3.1 仓库自动解锁

逻辑:订单创建,订单超时,库存自动解锁。上图路线1
在这里插入图片描述

1.3.2 订单关闭,库存解锁

逻辑:订单由于网络等原因,库存解锁消息 早于 订单释放解锁的信息。造成订单库存无法解锁。上图路线2
订单关闭(释放),库存解锁,发送消息
在这里插入图片描述

1)提交订单。submitOrder
       R r = wmsFeignService.orderLockStock(lockVo);
       System.out.println("r.getCode(): "+r.getCode() );
       if (r.getCode() == 0) {
         //锁定库存成功
         response.setOrder(order.getOrder());

         //todo 远程扣减积分,出现异常
//        int m = 10 / 0;//订单回滚,库存不滚。使用Seata分布式事务,让远程ware也回滚

         //Todo 订单创建成功,发送消息给MQ
         System.out.println("订单创建成功,发送消息给MQ");
         rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
          return response;
2).订单项目的RabbitMQ配置类
@Configuration
public class MyRabbitMQConfig {
    /**
     * 容器中的2个Queue、1个Exchange、2个Binding。 会自动创建(在RabbitMQ)不存在的情况下
     * RabbitMQ只要有, @Bean声明属性不会发生变化也不会覆盖
     * 采用:1个交换机、2个队列、2个绑定的模式
     * 原理:订单创建成功,先把消息放入延迟队列(死信队列)。如果过期时间到,把消息放入订单队列,等待消费者释放消息。
     */

    //死信队列(延迟队列)-存放原始的消息
    @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分钟
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
        return queue;
    }

    //普通队列(存放死信的队列)
    @Bean
    public Queue orderReleaseQueue() {
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }

    //TopicExchange。交换机
    @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);
    }

}
3).订单order项目的监听器
/**
 * @Description: 定时关闭订单
 **/
@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
        try {
            orderService.closeOrder(orderEntity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}
4).库存ware项目的RabbitMQ配置类
@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;

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

    //库存服务默认的交换机
    @Bean
    public Exchange stockEventExchange() {
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
        return topicExchange;
    }

    //普通队列(存放死信-超时的消息)
    @Bean
    public Queue stockReleaseStockQueue() {
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        Queue queue = new Queue("stock.release.stock.queue", true, false, false);
        return queue;
    }


    //延迟队列
    @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");
        // 消息过期时间 2分钟
        arguments.put("x-message-ttl", 120000);

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

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

    //交换机与普通队列绑定
    @Bean
    public Binding stockLocked() {
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
        Binding binding = new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);

        return binding;
    }
}

4)库存ware项目的库存监听器
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁
     * 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     * 2、订单失败
     * 库存锁定失败
     * 只要解锁库存的消息失败,一定要告诉服务解锁失败
     */
    //1.库存自动解锁
    @RabbitHandler
    public void handleLockedStockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("收到解锁库存的消息。。");
        try {
            wareSkuService.unLockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
     //2.订单创建,订单释放,库存解锁
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
        System.out.println("订单关闭准备解锁库存。。");
        try {
            wareSkuService.unLockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
5).订单服务的OrderServiceImpl的closeOrder
    public void closeOrder(OrderEntity entity) {
        //查询当前订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        //如果订单状态是刚创建,待付款0。关单
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
            System.out.println("关闭订单,并更新订单状态为:"+OrderStatusEnum.CANCLED.getMsg());
            //关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
            //TODO 再发给MQ(订单释放服务、库存解锁)
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }
/**
 * 库存自动解锁:
 * 1、下订单成功,库存解锁成功。接下来业务调用失败,导致订单回滚。
 *      之前锁定的库存需要自动解锁
 * 2、订单失败:
 *      锁库存失败
 * 只要解锁库存失败,一定要告诉服务解锁失败
 */
6).解锁库存的方法-WareSkuServiceImpl
    /**
     * 查询数据库,关于订单的解锁消息
     *     订单的解锁消息:
     *          1.有: 证明库存锁定成功。是否解锁库存,看订单情况
     *           1).订单不存在,必须解锁库存 ok
     *           2).有此订单。是否解锁库存,看订单的状态
     *              订单的状态:
     *                   已取消:说明用户取消了订单。必须解锁库存 ok
     *                   没取消:不能解锁库存
     *          2.没有:库存锁定失败,库存回滚了。这种情况无需解锁
     */
    //解锁库存
    @Override
    public void unLockStock(StockLockedTo to) {
        StockDetailTo detail = to.getDetail();//订单库存工作单详情
        Long detailId = detail.getId();
        //解锁

        WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
        if (byId != null) {
            //解锁
            //根据订单号获得订单信息orderSn,远程order
            Long taskId = to.getId();//订单工作单id
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(taskId);
            String orderSn = taskEntity.getOrderSn();
            R r = orderFeignService.getOrderStatus(orderSn);
            if (r.getCode() == 0) {
                //订单数据返回成功
                OrderVo data = r.getData(new TypeReference<OrderVo>() {
                });
                if (data == null || data.getStatus() == OrderStatusEnum.CANCLED.getCode()) {
                    //订单不存在,必须解锁库存
                    //订单被客户取消,必须解锁库存
                    if (byId.getLockStatus() == 1) {
                        //当前库存工作单详情,状态1(已锁定),才可以解锁
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                    }
                }
            } else {
                //拒绝消息后重新放入队列。让别人继续消费解锁
                throw new RuntimeException("远程服务失败");
            }
        } else {
            //不用解锁
        }
    }
7).关闭订单的方法-WareSkuServiceImpl
//解锁订单
    //防止订单服务卡顿,导致订单状态信息一直改不了,而库存信息优先到期。
    //结果:导致卡顿的订单,永远无法进行库存解锁
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void unLockStock(OrderTo to) {
        String orderSn = to.getOrderSn();
        //查下最新的库存解锁状态,防止重复解锁库存(订单工作单)
        WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);

        //按照工作单的id找到所有 没有解锁的库存,进行解锁
        Long id = orderTaskEntity.getId();
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id).eq("lock_status", 1));

        for (WareOrderTaskDetailEntity task : list) {
            //库存自动解锁
            unLockStock(task.getSkuId(), task.getWareId(), task.getSkuNum(), task.getId());
        }
    }

1.4 消息丢失、挤压、重复等

1).如何保证消息可靠性-消息丢失

在这里插入图片描述

1.1) 解决方案

在这里插入图片描述

2).如何保证消息可靠性-消息重复

在这里插入图片描述

2.1).解决方案

在这里插入图片描述

3).如何保证消息可靠性-消息积压

在这里插入图片描述

3.1).解决方案

在这里插入图片描述

4).最终的解决

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值