Spring RabbitMQ那些事(2-两种方式实现延时消息订阅)

目录

一、序言

业务开发中有很多延时操作的场景,比如最常见的超时订单自动关闭延时异步处理,我们常用的实现方式有:

  • 定时任务轮询(有延时)。
  • 借助Redission的延时队列
  • Redis的key过期事件通知机制(需开启key过期事件通知,对Redis有性能损耗)。
  • RocketMQ中定时消息推送(支持的时间间隔固定,不支持自定义)。
  • RabbitMQ中的死信队列和延迟消息交换机

其中用的最多的也是借助Redisson实现的数据结构延迟队列RabbitMQ中的死信队列来实现,今天我们通过RabbitMQ死信队列和延迟消息交换机(新特性)来实现延时消息推送。


二、死信交换机和消息TTL实现延迟消息

1、死信队列介绍

这种方式主要通过结合消息过期和私信交换机来实现延迟消息推送,首先先了解下哪些消息会进入死信队列:

  • 被消费者nack(negatively acknowleged)的消息。
  • TTL过期后未被消费的消息。
  • 超过队列长度限制后被丢弃的消息。

备注:更多信息请参考RabbitMQ中的 Dead Letter Exchange

2、代码示例

(1) 死信交换机配置

java 复制代码
@Configuration
protected static class DeadLetterExchangeConfig {

	@Bean
	public Queue deadLetterQueue(){
		return QueueBuilder.durable("dead-letter-queue").build();
	}

	@Bean
	public DirectExchange deadLetterExchange() {
		return ExchangeBuilder.directExchange("dead-letter-exchange").build();
	}

	@Bean
	public Binding bindQueueToDeadLetterExchange(Queue deadLetterQueue, DirectExchange deadLetterExchange) {
		return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with("dead-letter-routing-key");
	}

	@Bean
	public Queue normalQueue() {
		return QueueBuilder.durable("normal-queue")
			.deadLetterExchange("dead-letter-exchange")
			.deadLetterRoutingKey("dead-letter-routing-key")
			.build();
	}

}

(2) 消息生产者

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RabbitMqProducer {

	private final RabbitTemplate rabbitTemplate;
	
	public void sendMsgToDeadLetterExchange(String body, int timeoutInMillSeconds) {
		log.info("开始发送消息到dead letter exchange 消息体:{}, 消息延迟:{}ms, 当前时间:{}", body, timeoutInMillSeconds, LocalDateTime.now());
		MessageProperties messageProperties = MessagePropertiesBuilder.newInstance().setExpiration(String.valueOf(timeoutInMillSeconds)).build();
		Message message = MessageBuilder.withBody(body.getBytes(StandardCharsets.UTF_8)).andProperties(messageProperties).build();
		rabbitTemplate.send("normal-queue", message);
	}
	
}

(3) 消息消费者

java 复制代码
@Slf4j
@Component
public class RabbitMqConsumer {

	@RabbitListener(queues = "dead-letter-queue")
	public void handleMsgFromDeadLetterQueue(String msg) {
		log.info("Message received from dead-letter-queue, message body: {}, current time:{}", msg, LocalDateTime.now());
	}
}

3、测试用例

java 复制代码
@RestController
@RequiredArgsConstructor
public class RabbitMsgController {

	private final RabbitMqProducer rabbitMqProducer;
	
	@RequestMapping("/exchange/dead-letter")
	public ResponseEntity<String> sendMsgToDeadLetterExchange(String body, int timeout) {
		rabbitMqProducer.sendMsgToDeadLetterExchange(body, timeout);
		return ResponseEntity.ok("消息发送到死信交换机成功");
	}
	
}

浏览器访问http://localhost:8080/exchange/dead-letter?body=hello&timeout=5000,可以看到消息被延迟5s处理。

java 复制代码
2023-11-26 11:50:33.041  INFO 19152 --- [nio-8080-exec-7] c.u.r.i.producer.RabbitMqProducer        : 开始发送消息到dead letter exchange 消息体:hello, 消息延迟:5000ms, 当前时间:2023-11-26T11:50:33.041
2023-11-26 11:50:38.054  INFO 19152 --- [ntContainer#4-4] c.u.r.i.consumer.RabbitMqConsumer        : Message received from dead-letter-queue, message body: hello, current time:2023-11-26T11:50:38.054

三、延迟消息交换机实现延迟消息

上面通过消息TTL和死信交换机实现延迟消息的解决方案是由一个叫James Carr的人提出来的,后来RabbitMQ提供了一个开箱即用的解决方案,通过延时消息插件来实现。

该插件以前被当做是试验性产品,但是现在已经可以投产使用了。(PS:2015年4月16号就已经有该插件文档

Spring AMQP 中,同样提供了对该延时消息插件的支持,并且在RabbitMQ 3.6.0版本就已经测试通过。

1、安装延时消息插件

该延时消息插件为社区插件,因此需要自己手动下载安装的RabbMQ版本对应的插件,下载地址:RabbitMQ延时消息插件releases

我安装的RabbitMQ版本为3.9.9,3.9.0版本的插件对所有3.9.x版本的RabbitMQ都支持。

下载完后把.ez结尾的插件复制RabbitMQ的插件目录下,插件目录为/usr/lib/rabbitmq/plugins

通过命令rabbitmq-plugins enable rabbitmq_delayed_message_exchange安装该插件,通过命令rabbitmq-plugins list查看插件列表,可以看到该延时消息插件已经成功安装。

2、代码示例

(1) 延时消息交换机配置

java 复制代码
@Configuration
protected static class DelayedMsgExchangePluginConfig {

	@Bean
	public Queue delayedQueue() {
		return QueueBuilder.durable("delayed-queue").build();
	}

	@Bean
	public DirectExchange delayedExchange() {
		return ExchangeBuilder.directExchange("delayed-exchange").delayed().build();
	}

	@Bean
	public Binding bindDelayedQueueToDelayedChange(Queue delayedQueue, DirectExchange delayedExchange) {
		return BindingBuilder.bind(delayedQueue).to(delayedExchange).with("delayed-routing-key");
	}
}

备注:延时交换机的类型可以为DirectExchage、TopicExcahgeFanoutExchange,这些都支持。

(2) 消息生产者

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RabbitMqProducer {

	private final RabbitTemplate rabbitTemplate;

	public void sendDelayedMsg(String body, int timeoutInMillSeconds) {
		log.info("开始发送消息到delayed-exchange 消息体:{}, 消息延迟:{}ms, 当前时间:{}", body, timeoutInMillSeconds, LocalDateTime.now());
		MessageProperties messageProperties = new MessageProperties();
		messageProperties.setDelay(timeoutInMillSeconds);
		Message message = MessageBuilder.withBody(body.getBytes(StandardCharsets.UTF_8)).andProperties(messageProperties).build();
		rabbitTemplate.send("delayed-exchange", "delayed-routing-key", message);
	}
}

(3) 消息消费者

java 复制代码
@Slf4j
@Component
public class RabbitMqConsumer {

	@RabbitListener(queues = "delayed-queue")
	public void handleMsgFromDelayedQueue(String msg) {
		log.info("Message received from delayed-queue, message body: {}, current time:{}", msg, LocalDateTime.now());
	}
}

3、测试用例

java 复制代码
@RestController
@RequiredArgsConstructor
public class RabbitMsgController {

	private final RabbitMqProducer rabbitMqProducer;

	@RequestMapping("/exchange/delayed")
	public ResponseEntity<String> sendMsgToHeadersExchange(String body, int timeout) {
		rabbitMqProducer.sendDelayedMsg(body, timeout);
		return ResponseEntity.ok("消息发送到延迟交换机成功");
	}
	
}

浏览器访问http://localhost:8080/exchange/dead-letter?body=hello&timeout=5000,可以看到消息被延迟5s处理。

javascript 复制代码
2023-11-26 13:02:07.816  INFO 26524 --- [nio-8080-exec-3] c.u.r.i.producer.RabbitMqProducer        : 开始发送消息到delayed-exchange 消息体:Hello, 消息延迟:5000ms, 当前时间:2023-11-26T13:02:07.816
2023-11-26 13:02:12.830  INFO 26524 --- [ntContainer#5-5] c.u.r.i.consumer.RabbitMqConsumer        : Message received from delayed-queue, message body: Hello, current time:2023-11-26T13:02:12.829

四、两种实现方式优缺点

1、延时消息插件

  • 优点:配置更加简单,少配置1个过期消息接收队列,且语义更明确,容易定位消息出入口。
  • 缺点:延时消息插件对RabbitMQ版本有要求,只有RabbitMQ 3.8.x及以上版本支持。

2、TLL&死信交换机

  • 优点:基本适用于所有RabbitMQ版本。
  • 缺点:配置相对来说复杂一些,还有就是我们最开始提到的,不只是TTL过期的消息才会进入死信队列,还有超出队列限制nack的消息也会进入死信队列,触发的条件没那么纯粹。
相关推荐
蔡俊锋2 小时前
Javar如何用RabbitMQ订单超时处理
java·python·rabbitmq·ruby
小兔兔吃萝卜5 小时前
Spring 创建 Bean 的 8 种主要方式
java·后端·spring
AAA修煤气灶刘哥6 小时前
面试官: SpringBoot自动配置的原理是什么?从启动到生效,一文讲透
后端·spring·面试
qq_三哥啊8 小时前
【IDEA】设置Debug调试时调试器不进入特定类(Spring框架、Mybatis框架)
spring·intellij-idea·mybatis
别惹CC9 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
人工智能·spring boot·spring
寒士obj9 小时前
Spring事物
java·spring
IT毕设实战小研17 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
甄超锋18 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
Java小白程序员1 天前
Spring Framework:Java 开发的基石与 Spring 生态的起点
java·数据库·spring
甄超锋1 天前
Java Maven更换国内源
java·开发语言·spring boot·spring·spring cloud·tomcat·maven