赞
踩
1. 订单超时处理
延迟队列可以用于处理订单超时问题。当用户下单后,将订单信息放入延迟队列,并设置一定的超时时间。如果在超时时间内用户未支付订单,消费者会从延迟队列中获取到该订单,并执行相应的处理操作,如取消订单、释放库存等。
2. 优惠券过期提醒
延迟队列可以用于优惠券的过期提醒功能。将即将过期的优惠券信息放入延迟队列,并设置合适的延迟时间。当延迟时间到达时,消费者将提醒用户优惠券即将过期,引导用户尽快使用。
3. 异步通知与提醒
延迟队列可以用于异步通知和提醒功能。例如,当用户完成某个操作后,系统可以将相关通知消息放入延迟队列,并设置一定的延迟时间,以便在合适的时机发送通知给用户。
1. 创建延迟队列服务类
@Service public class DelayQueueService { private static final String DELAY_QUEUE_KEY = "delay_queue"; @Autowired private RedisTemplate<String, String> redisTemplate; public void addToDelayQueue(String message,long delayTime){ redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY,message,System.currentTimeMillis()+delayTime); } public void processDelayedMessage(){ //reverseRangeByScore 从高到低 //rangeByScore 从低到高 Set<String> messages = redisTemplate.opsForZSet().rangeByScore(DELAY_QUEUE_KEY, 0, System.currentTimeMillis()); for(String message:messages){ //处理消息 System.out.println(message); redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY,message); } } }
2. 配置定时任务或消息消费者
使用Spring Boot的定时任务或消息队列框架,定时调用延迟队列服务类的轮询方法或监听指定的消息队列,可以将轮训粒度放到1s一次。
@Component
public class DelayQueueSchedule {
@Autowired
private DelayQueueService delayQueueService;
// 每隔一段时间进行轮询并处理延迟消息
@Scheduled(fixedDelay = 1000)
public void pollAndProcessDelayedMessages() {
delayQueueService.pollAndProcessDelayedMessages();
}
}
然后在启动类上通过@EnableScheduling注解开启任务调度能力。
缺点:
使用ZSET(有序集合,Sorted Set)来实现延迟任务调度(如订单超时取消)是一种有效的方法,但它也有一些缺点和限制:
死信,顾名思义就是无法被消费的消息。一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致queu 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
列出2种实现方式。
(1)使用Time To Live(TTL) + Dead Letter Exchanges(DLX)死信队列组合实现延迟队列的效果。
(2)使用RabbitMQ官方延迟插件rabbitmq_delayed_message_exchange,实现延时队列效果。
由于TTL(生存时间)过期导致的死信,就是我们实现延迟队列的的方式。
我们需要声明如下形式的交互机和队列,以及对应的routing key,并进行绑定:
上图绑定的代码如下所示
@Configuration public class DeadQueueConfig { //普通交换机及队列 public static final String X_EXCHANGE = "X"; public static final String QUEUE_A = "QA"; public static final String QUEUE_B = "QB"; //死信交换机及队列 public static final String Y_DEAD_LETTER_EXCHANGE = "Y"; public static final String DEAD_LETTER_QUEUE = "QD"; //通用队列 public static final String QUEUE_C = "QC"; // 声明 xExchange @Bean("xExchange") public DirectExchange xExchange() { return new DirectExchange(X_EXCHANGE); } //声明队列 A ttl 为 10s 并绑定到对应的死信交换机 @Bean("queueA") public Queue queueA() { Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "YD"); //声明队列的 TTL args.put("x-message-ttl", 10000); return QueueBuilder.durable(QUEUE_A).withArguments(args).build(); } //声明队列A绑定X交换机 路由为XA @Bean public Binding queueABingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){ return BindingBuilder.bind(queueA).to(xExchange).with("XA"); } //声明队列 B ttl 为 40s 并绑定到对应的死信交换机 @Bean("queueB") public Queue queueB() { Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "YD"); //声明队列的 TTL args.put("x-message-ttl", 40000); return QueueBuilder.durable(QUEUE_B).withArguments(args).build(); } //声明队列 B 绑定 X 交换机 @Bean public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queue1B).to(xExchange).with("XB"); } //声明通用队列C 不设ttl,由消费者决定ttl @Bean("queueC") public Queue queueC() { Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "YD"); return QueueBuilder.durable(QUEUE_C).withArguments(args).build(); } // 声明队列 C 绑定 X 交换机 @Bean public Binding queuecBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueC).to(xExchange).with("XC"); } // 声明 死信队列交换机 @Bean("yExchange") public DirectExchange yExchange() { return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); } //声明死信队列 QD @Bean("queueD") public Queue queueD() { return new Queue(DEAD_LETTER_QUEUE,true); } //声明死信队列 QD 绑定关系 @Bean public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange) { return BindingBuilder.bind(queueD).to(yExchange).with("YD"); } }
其中,QD为死信队列。当QA和QB队列中的消息,达到设定的TTL(10s和40s)后,将进入指定的死信队列QD。该方法缺点就是一个TTL对应一个队列
其中的QC作为通用的队列,即在消费者处指定消息对应的TTL,TTL过期后转入死信队列。使用该通用队列可以避免每增加一个新的时间需求,就要新增一个队列的问题。但该方法由于队列先进先出的性质,会导致一定的问题:
即先发出一个TTL为10s的消息a,进入队列;再马上发出一个TTL为2s的消息b,进入队列。由于队列的性质,会在消息a的TTL结束后,a进入死信队列后,b才会进入死信队列。而不是根据TTL的时间,b比a先进入死信队列。
声明交换机、队列,并绑定成功后,编写死信队列消费者代码;
@Component
@Slf4j
public class DeadQueueConsumer {
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息:{}", new Date().toString(), msg);
}
}
在controller中编写生产者代码,进行测试:
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public String sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message);
return "finish";
}
结果如图:
测试通用队列QC的效果:
@GetMapping("/send/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message, @PathVariable String ttlTime) {
rabbitTemplate.convertAndSend("X", "XC", message, correlationData -> {
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(), ttlTime, message);
}
结果如下图
可以看到, 两条消息几乎同时到达死信队列,因为TTL为2s的消息由于被堵在TTL为10s的消息后导致。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。