订单超时未支付

249 阅读5分钟

业务需求:

用戶在购买商品的时候通常会预购然后没付款,没付款的订单通常会被设置一个自动超时时间如30分钟后超时,所以我们要在订单到30分钟后自动将超时的订单取消。

调研:

在做这一部分的时候,也考虑了几个方案。

1. JUC(DelayQueue)

JDK延时队列DelayQueue是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。

DelayQueue简介
  • DelayQueue是java并发包下的延时阻塞队列,常用于实现定时任务。
  • DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素, 如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。
  • DelayQueue主要用于两个方面:- 缓存:清掉缓存中超时的缓存数据- 任务超时处理
  • DelayQueue实现了BlockingQueue,所以它是一个阻塞队列。
  • DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口。

想法是使用一个队列,将订单放入队列之中,设置超时秒数,这个队列会自动处理超时订单。

问题:

  1. JUC是纯内存操作一旦系统宕机数据将全部丢失。
2. Redis Key过期事件方案

可以通过配置 notify-keyspace-events 选项,我们可以开启对键空间事件的监听,从而实现实时监控和处理键空间的操作。

notify-keyspace-events 可以设置为以下各项的组合: – K 键空间通知,所有通知事件都以keyspace@ 为前缀,并通过 PUBLISH 命令发送 –E键事件通知,所有通知事件都以keyevent@ 为前缀,并通过 PUBLISH 命令发送 – g DEL、EXPIRE、RENAME等生成通知 – s SET、EXPIREAT等生成通知 – h HSET、HDEL等生成通知 – l LSET、LREM等生成通知 – z ZADD、ZREM等生成通知 – x过期事件:当键被设置了过期时间时,会生成通知 – e 驱逐事件:当键因为空间被驱逐出去时,会生成通知 – A 任何事件都会生成通知

问题:

  1. Redis Key过期前程序突然宕机将造成数据丢失。
  2. Redis缓存所有的Key都会被监听,需要自己处理Key去匹配不够灵活

RabbitMQ方案

1. 使用TTL+DLX的方式实现延迟队列

创建两个队列,一个是普通的队列,设置一个TTL(time to live存活时间),一个是死信队列,TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter。

在RabbitMQ中,一共有三种消息的“死亡”形式:

  1. 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
  2. 消息因为设置了TTL而过期。
  3. 消息进入了一条已经达到最大长度的队列。

另一个队列是死信队列,DLX(Dead Letter Exchange,死信交换机,配置后死信会被推到这里)。

没有超时的话就没事了。

  1. 定义死信交换机(DLX)

    • 创建一个交换机,用于接收超时的消息。
  2. 创建死信队列

    • 创建一个队列并将其绑定到DLX。
  3. 设置延迟队列

    • 创建一个普通队列,设置其x-message-ttl属性,这个属性定义了消息在队列中存活的时间。
  4. 发送订单消息

    • 当订单创建时,将订单消息发送到延迟队列,并设置适当的TTL值,这个值基于业务规则确定的订单超时时间。
  5. 消息超时

    • 消息在延迟队列中等待,直到TTL时间到期。
  6. 消息转换为死信

    • 当消息的TTL时间到期后,消息会变成死信,并根据设置的DLX策略发送到DLX。
  7. 死信队列接收

    • DLX接收到死信消息后,将其路由到绑定的死信队列。
  8. 消费死信消息

    • 应用程序中的消费者监听死信队列,接收超时的订单消息。
  9. 执行订单取消逻辑

    • 消费者接收到订单消息后,执行订单取消操作,如更新订单状态、通知用户等。
2.使用延迟插件实现延迟队列,其余不变
插件安装

我这里安装的RabbitMQ版本为3.8.8,这里我在发行版本中下载版本为: rabbitmq-delayed-message-exchange v3.8.x

  1. 将下载好的插件(rabbitmq_delayed_message_exchange-3.8.0.ez)复制到plugins目录下(C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.8\plugins); 进入到sbin目录(C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.8\sbin)打开cmd窗口执行命令开启插件:

     C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.8\sbin>rabbitmq-plugins enable rabbitmq_delayed_message_exchange
     Enabling plugins on node rabbit@LX-P1DMPLUV:
     rabbitmq_delayed_message_exchange
     The following plugins have been configured:
       rabbitmq_delayed_message_exchange
       rabbitmq_management
       rabbitmq_management_agent
       rabbitmq_web_dispatch
     Applying plugin configuration to rabbit@LX-P1DMPLUV...
     The following plugins have been enabled:
       rabbitmq_delayed_message_exchange
     ​
     started 1 plugins.
    
  2. 继续输入命令重启RabbitMQ:rabbitmq-service restart

代码编写demo
  1. 配置队列:

     public class DelayedQueueConfig {
         // 延迟队列
         public static final String DELAYED_QUEUE = "delayed.queue";
         // 延迟队列交换机
         public static final String DELAYED_EXCHANGE = "delayed.exchange";
         // 延迟队列路由KEY
         public static final String DELAYED_ROUTING_KEY = "delayed.routing.key";
         // 订单过期时间
         public static final String ORDER_OUTIME ="10000";
     ​
     }
    
  2. 消费者定义,延迟消息订单超时消费队列定义:

     @Slf4j
     @Component
     public class DelayedConsumerAnnotatedEdition {
         /**
          * 延迟队列交换机类型必须为:x-delayed-message
          * x-delayed-type 必须设置否则将会报错
          */
         @RabbitListener(bindings = {
                 @QueueBinding(value = @Queue(value = DelayedQueueConfig.DELAYED_QUEUE),
                         exchange = @Exchange(value = DelayedQueueConfig.DELAYED_EXCHANGE,type = "x-delayed-message",
                         arguments = {@Argument(name = "x-delayed-type", value = ExchangeTypes.DIRECT)}),
                         key = {DelayedQueueConfig.DELAYED_ROUTING_KEY}
                 )
         })
         
         //处理超时订单
         @RabbitHandler
         public void delayedConsumer(String context, Message message, Channel channel) throws Exception {
             log.info("当前时间:{} 订单取消订单号:{}", DateUtil.getCuurentDateStr(),context);
             channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//仅确认本条消息
         }
     }
    
  3. 定义入口请求用于向延迟消息队列投递消息:

     @Slf4j
     @RestController
     @RequestMapping("rabbit/dealyed/queue")
     @AllArgsConstructor
     public class RabbitDelayedQueueProducer {
         private RabbitTemplate rabbitTemplate;
         /**
          * 投递订单到延迟消息队列中
          * @param orderId 订单ID
          */
         @GetMapping("/sendMessage")
         public String sendMessage(@RequestParam(value = "orderId") String orderId){
             rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE, DelayedQueueConfig.DELAYED_ROUTING_KEY, orderId, new MessagePostProcessor() {
                 @Override
                 public Message postProcessMessage(Message message) throws AmqpException {
                     // 设置消息超时时间
                     message.getMessageProperties().setHeader("x-delay", DelayedQueueConfig.ORDER_OUTIME);
                     return message;
                 }
             });
             log.info("当前时间:{} 订单号:{}", DateUtil.getCuurentDateStr(),orderId);
             return "OK";
         }
     }