业务需求:
用戶在购买商品的时候通常会预购然后没付款,没付款的订单通常会被设置一个自动超时时间如30分钟后超时,所以我们要在订单到30分钟后自动将超时的订单取消。
调研:
在做这一部分的时候,也考虑了几个方案。
1. JUC(DelayQueue)
JDK延时队列DelayQueue是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。
DelayQueue简介
- DelayQueue是java并发包下的延时阻塞队列,常用于实现定时任务。
- DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素, 如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。
- DelayQueue主要用于两个方面:- 缓存:清掉缓存中超时的缓存数据- 任务超时处理
- DelayQueue实现了BlockingQueue,所以它是一个阻塞队列。
- DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口。
想法是使用一个队列,将订单放入队列之中,设置超时秒数,这个队列会自动处理超时订单。
问题:
- 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 任何事件都会生成通知
问题:
- Redis Key过期前程序突然宕机将造成数据丢失。
- Redis缓存所有的Key都会被监听,需要自己处理Key去匹配不够灵活
RabbitMQ方案
1. 使用TTL+DLX的方式实现延迟队列
创建两个队列,一个是普通的队列,设置一个TTL(time to live存活时间),一个是死信队列,TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter。
在RabbitMQ中,一共有三种消息的“死亡”形式:
- 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
- 消息因为设置了TTL而过期。
- 消息进入了一条已经达到最大长度的队列。
另一个队列是死信队列,DLX(Dead Letter Exchange,死信交换机,配置后死信会被推到这里)。
没有超时的话就没事了。
-
定义死信交换机(DLX) :
- 创建一个交换机,用于接收超时的消息。
-
创建死信队列:
- 创建一个队列并将其绑定到DLX。
-
设置延迟队列:
- 创建一个普通队列,设置其
x-message-ttl属性,这个属性定义了消息在队列中存活的时间。
- 创建一个普通队列,设置其
-
发送订单消息:
- 当订单创建时,将订单消息发送到延迟队列,并设置适当的TTL值,这个值基于业务规则确定的订单超时时间。
-
消息超时:
- 消息在延迟队列中等待,直到TTL时间到期。
-
消息转换为死信:
- 当消息的TTL时间到期后,消息会变成死信,并根据设置的DLX策略发送到DLX。
-
死信队列接收:
- DLX接收到死信消息后,将其路由到绑定的死信队列。
-
消费死信消息:
- 应用程序中的消费者监听死信队列,接收超时的订单消息。
-
执行订单取消逻辑:
- 消费者接收到订单消息后,执行订单取消操作,如更新订单状态、通知用户等。
2.使用延迟插件实现延迟队列,其余不变
插件安装
我这里安装的RabbitMQ版本为3.8.8,这里我在发行版本中下载版本为: rabbitmq-delayed-message-exchange v3.8.x
-
将下载好的插件(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. -
继续输入命令重启RabbitMQ:rabbitmq-service restart
代码编写demo
-
配置队列:
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"; } -
消费者定义,延迟消息订单超时消费队列定义:
@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);//仅确认本条消息 } } -
定义入口请求用于向延迟消息队列投递消息:
@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"; } }