Springboot整合Rabbitmq实现延时队列之rabbitmq_delayed_message_exchange插件方式
很多时候我们想定时去做某件事情的时候我们会首先想到定时任务,quartz是个不错的选择,但是也有缺点,假如配置在项目中,集群部署会有重复执行的问题,如果持久化在mysql中,解决了集群的问题,但是过于依赖mysql,耦合严重,当然还有日志量庞大、执行时间精度、过于耗费系统资源等等问题。所以这时候使用消息队列中间件的的延时队列就是一个很好得解决方案,我们设置要触发消费的时间和必要的参数入队mq,到时监听queue的消费者自然拿到消息然后去走业务流程,这里介绍的是基于rabbitmq中间件实现的TTL版的延时队列。
什么是TTL?
先简单介绍下rabbitmq执行的流程,和Spring boot整合ActiveMQ不太一样,除了队列(queue)之外还引入了交换机(exchange)的概念。
rabbitmq的交换机有4种模式,我不详细介绍,简单说下大体执行流程:
①:生产者将消息(msg)和路由键(routekey)发送指定的交换机(exchange)上
②:交换机(exchange)根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它
③:队列(queue)再把消息发送给监听它的消费者(customer)
那么延时队列TTL又是什么呢?这里引入了一个死信(死亡信息)的概念,有死信必定有死亡时间,也就是我们希望延时多久的时间:

②:死信交换机(delayexchange)根据路由键(routekey1)找到绑定自己的死信队列(delayqueue)并把消息给它
③:消息(msg)到期死亡变成死信转发给死信接收交换机(delayexchange)
④:死信接收交换机(receiveexchange)根据路由键(routekey2)找到绑定自己的死信接收队列(receivequeue)并把消息给它
⑤:死信接收队列(receivequeue)再把消息发送给监听它的消费者(customer)
ps:延时队列也叫死信队列。基于TTL模式的延时队列会涉及到2个交换机、2个路由键、2个队列…emmmmm比较麻烦
举个栗子:延时队列里先后进入A,B,C三条消息,存活时间是3h,2h,1h,结果到了1小时C不会死,到了2hB不会死,到了3小时A死了,同时B,C也死了,意味着3h后A,B,C才能消费,很坑!!!
我本来使用时候以为会像redis的存活时间一样,内部维护一个定时器去扫描死亡时间然后变成死信转发,结果不是。。。
至于怎么解决这个问题,一个队列里可以放不同死亡时间的消息,还能够异步死亡转发,请看下面:
基于插件方式实现流程:
这里和TTL方式有个很大的不同就是TTL存放消息在死信队列(delayqueue)里,二基于插件存放消息在延时交换机里(x-delayed-message exchange)。

②:延时交换机(exchange)存储消息等待消息到期根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它
③:队列(queue)再把消息发送给监听它的消费者(customer)

下载的插件放到rabbitmq的plugins里,执行命令安装插件:
1 | rabbitmq-plugins enable rabbitmq_delayed_message_exchange |
流程介绍完了,看下具体代码吧!
1.首先pom依赖:
1 2 3 4 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> |
2.配置文件配置rabbitmq的信息
1 2 3 4 5 6 7 8 | # rabbitmq spring.rabbitmq.host=localhost spring.rabbitmq.port= 5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/ # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none spring.rabbitmq.listener.simple.acknowledge-mode=manual |
3.编写rabbitmq配置类,声明几个bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * rabbitmq配置类 * 员工系统配置延时队列 * @author 47 * @date 2020/1/7 */ @Configuration public class RabbitUserConfig { /** * 延时队列交换机 * 注意这里的交换机类型:CustomExchange * @return */ @Bean public CustomExchange delayExchange(){ Map<String, Object> args = new HashMap<>(); args.put( "x-delayed-type" , "direct" ); return new CustomExchange( "delay_exchange" , "x-delayed-message" , true , false ,args); } /** * 延时队列 * @return */ @Bean public Queue delayQueue(){ return new Queue( "delay_queue" , true ); } /** * 给延时队列绑定交换机 * @return */ @Bean public Binding cfgDelayBinding(Queue cfgDelayQueue,CustomExchange cfgUserDelayExchange){ return BindingBuilder.bind(cfgDelayQueue).to(cfgUserDelayExchange).with( "delay_key" ).noargs(); } } |
4.编写rabbitmq生产者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /** * rabbitMq生产者类 * @author 47 * @date 2020/1/17 */ @Component @Slf4j public class RabbitProduct{ @Autowired private RabbitTemplate rabbitTemplate; public void sendDelayMessage(List<Integer> list) { //这里的消息可以是任意对象,无需额外配置,直接传即可 log.info( "===============延时队列生产消息====================" ); log.info( "发送时间:{},发送内容:{}" , LocalDateTime.now(), list.toString()); this .rabbitTemplate.convertAndSend( "delay_exchange" , "delay_key" , list, message -> { //注意这里时间可以使long,而且是设置header message.getMessageProperties().setHeader( "x-delay" , 60000 ); return message; } ); log.info( "{}ms后执行" , 60000 ); } |
5.编写rabbitmq消费者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * activeMq消费者类 * @author 47 * @date 2020/1/7 */ @Component @Slf4j public class RabbitConsumer { @Autowired private CcqCustomerCfgService ccqCustomerCfgService; /** * 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK * 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完 * 解决方案:手动ACK,或者try-catch 然后在 catch 里面将错误的消息转移到其它的系列中去 * spring.rabbitmq.listener.simple.acknowledge-mode = manual * @param list 监听的内容 */ @RabbitListener (queues = "delay_queue" ) public void cfgUserReceiveDealy(List<Integer> list, Message message, Channel channel) throws IOException { log.info( "===============接收队列接收消息====================" ); log.info( "接收时间:{},接受内容:{}" , LocalDateTime.now(), list.toString()); //通知 MQ 消息已被接收,可以ACK(从队列中删除)了 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false ); try { dosomething..... } catch (Exception e) { log.error( "============消费失败,尝试消息补发再次消费!==============" ); log.error(e.getMessage()); /** * basicRecover方法是进行补发操作, * 其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer(集群)接收到, * 设置为false是只补发给当前的consumer */ channel.basicRecover( false ); } } } |
6.编写测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** * @author 47 * @date 2020/1/7 */ @RestController @RequestMapping ( "/test" ) public class TestController { @Autowired private RabbitProduct rabbitProduct; @GetMapping ( "/sendMessage" ) public void sendMessage(){ List<Integer> list = new ArrayList<>(); list.add( 1 ); list.add( 2 ); rabbitProduct.sendDelayMessage(list); } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具