RabbitMQ实现延时消息的两种方法
RabbitMQ实现延时消息的两种方法
1、死信队列
1.1消息什么时候变为死信(dead-letter)
- 消息被否定接收,消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。
- 消息在队列里得时间超过了该消息设置的过期时间(TTL)。
- 消息队列到达了它的最大长度,之后再收到的消息。
1.2死信队列的原理
当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的"x-dead-letter-exchange"参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。
1.3 代码实现
- 引入amqp依赖
- 声明交换机,队列
package com.lank.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author lank
* @since 2020/12/14 10:44
*/
@Configuration
public class RabbitmqConfig {
//死信交换机,队列,路由相关配置
public static final String DLK_EXCHANGE = "dlk.exchange";
public static final String DLK_ROUTEKEY = "dlk.routeKey";
public static final String DLK_QUEUE = "dlk.queue";
//业务交换机,队列,路由相关配置
public static final String DEMO_EXCHANGE = "demo.exchange";
public static final String DEMO_QUEUE = "demo.queue";
public static final String DEMO_ROUTEKEY = "demo.routeKey";
//延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
public static final String DMP_EXCHANGE = "dmp.exchange";
public static final String DMP_ROUTEKEY = "dmp.routeKey";
public static final String DMP_QUEUE = "dmp.queue";
@Bean
public DirectExchange demoExchange(){
return new DirectExchange(DEMO_EXCHANGE,true,false);
}
@Bean
public Queue demoQueue(){
//只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机
Map<String,Object> map = new HashMap<>(1);
map.put("x-dead-letter-exchange",DLK_EXCHANGE);
//该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key
map.put("x-dead-letter-routing-key",DLK_ROUTEKEY);
return new Queue(DEMO_QUEUE,true,false,false,map);
}
@Bean
public Binding demoBind(){
return BindingBuilder.bind(demoQueue()).to(demoExchange()).with(DEMO_ROUTEKEY);
}
@Bean
public DirectExchange dlkExchange(){
return new DirectExchange(DLK_EXCHANGE,true,false);
}
@Bean
public Queue dlkQueue(){
return new Queue(DLK_QUEUE,true,false,false);
}
@Bean
public Binding dlkBind(){
return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY);
}
//延迟插件使用
//1、声明一个类型为x-delayed-message的交换机
//2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
@Bean
public CustomExchange dmpExchange(){
Map<String, Object> arguments = new HashMap<>(1);
arguments.put("x-delayed-type", "direct");
return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
}
@Bean
public Queue dmpQueue(){
return new Queue(DMP_QUEUE,true,false,false);
}
@Bean
public Binding dmpBind(){
return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
}
}
- 声明一个类用于发送带过期时间的消息
package com.lank.demo.rabbitmq;
import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author lank
* @since 2020/12/14 10:33
*/
@Component
@Slf4j
public class MessageSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//使用死信队列发送消息方法封装
public void send(String message,Integer time){
String ttl = String.valueOf(time*1000);
//exchange和routingKey都为业务的就可以,只需要设置消息的过期时间
rabbitTemplate.convertAndSend(RabbitmqConfig.DEMO_EXCHANGE, RabbitmqConfig.DEMO_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置消息的过期时间,是以毫秒为单位的
message.getMessageProperties().setExpiration(ttl);
return message;
}
});
log.info("使用死信队列消息:{}发送成功,过期时间:{}秒。",message,time);
}
//使用延迟插件发送消息方法封装
public void send2(String message,Integer time){
rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
message.getMessageProperties().setHeader("x-delay",time*1000);
return message;
}
});
log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);
}
}
- 编写一个类用于消费消息
package com.lank.demo.rabbitmq;
import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author lank
* @since 2020/12/15 15:57
*/
@Component
@Slf4j
public class MessageReceiver {
@RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DLK_QUEUE)
public void onMessage(Message message){
log.info("使用死信队列,收到消息:{}",new String(message.getBody()));
}
@RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
public void onMessage2(Message message){
log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
}
}
- 编写Controller调用发送消息方法测试结果
package com.lank.demo.controller;
import com.lank.demo.rabbitmq.MessageSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lank
* @since 2020/12/14 17:05
*/
@RestController
public class MessageController {
@Autowired
public MessageSender messageSender;
//死信队列controller
@GetMapping("/send")
public String send(@RequestParam String msg,Integer time){
messageSender.send(msg,time);
return "ok";
}
//延迟插件controller
@GetMapping("/send2")
public String sendByPlugin(@RequestParam String msg,Integer time){
messageSender.send2(msg,time);
return "ok";
}
}
- 配置文件application.properties
server.port=4399
#virtual-host使用默认的/就好,如果需要/demo需自己在控制台添加
spring.rabbitmq.virtual-host=/demo
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
- 启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。


- 在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
2020-12-16 22:47:28.071 INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:hello发送成功,过期时间:5秒。
2020-12-16 22:47:33.145 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:hello
1.4死信队列的一个小坑
当我往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费。
依次发送两个请求http://localhost:4399/send?msg=消息A&time=30和http://localhost:4399/send?msg=消息B&time=10,消息A先发送,过期时间30S,消息B后发送,过期时间10S,我们想要的结果应该是10S收到消息B,30S后收到消息A,但结果并不是,控制台输出如下:
2020-12-16 22:54:47.339 INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息A发送成功,过期时间:30秒。
2020-12-16 22:54:54.278 INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息B发送成功,过期时间:10秒。
2020-12-16 22:55:17.356 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息A
2020-12-16 22:55:17.357 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息B
消息A30S后被成功消费,紧接着消息B被消费。因此当我们使用死信队列时应该注意是否消息的过期时间都是一样的,比如订单超过10分钟未支付修改其状态。如果当一个队列各个消息的过期时间不一致时,使用死信队列就可能达不到延时的作用。这时候我们可以使用延时插件来实现这需求。
2 、延时插件
RabbitMQ Delayed Message Plugin是一个rabbitmq的插件,所以使用前需要安装它,可以参考的GitHub地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
2.1如何实现
- 安装好插件后只需要声明一个类型type为"x-delayed-message"的exchange,并且在其可选参数下配置一个key为"x-delayed-typ",值为交换机类型(topic/direct/fanout)的属性。
- 声明一个队列绑定到该交换机
- 在发送消息的时候消息的header里添加一个key为"x-delay",值为过期时间的属性,单位毫秒。
- 代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。
- 启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机。


- 继续在浏览器中发送两个请求http://localhost:4399/send2?msg=消息A&time=30和http://localhost:4399/send2?msg=消息B&time=10,控制台输出如下,不会出现死信队列出现的问题:
2020-12-16 23:31:19.819 INFO 13304 --- [nio-4399-exec-9] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息A发送成功,过期时间:30秒。
2020-12-16 23:31:27.673 INFO 13304 --- [io-4399-exec-10] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息B发送成功,过期时间:10秒。
2020-12-16 23:31:37.833 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息B
2020-12-16 23:31:49.917 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息A
死信交换机官网介绍:https://www.rabbitmq.com/dlx.html
延时插件GitHub:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
RabbitMQ实现延时消息的两种方法的更多相关文章
- RabbitMQ Consumer获取消息的两种方式(poll,subscribe)解析
以下转自:http://blog.csdn.net/yangbutao/article/details/10395599 rabbitMQ中consumer通过建立到queue的连接,创建channe ...
- C#实现在Form上截取消息的两种方法
比较常用的是重载Form的DefWndProc方法,例如截取鼠标按下的消息: protected override void DefWndProc(ref Message m) { if ( m.Ms ...
- C#代码像QQ的右下角消息框一样,无论现在用户的焦点在哪个窗口,消息框弹出后都不影响焦点的变化,那么有两种方法
你QQ的右下角消息框一样,无论现在用户的焦点在哪个窗口,消息框弹出后都不影响焦点的变化,那么有两种方法: 要么重写需要弹出的窗体的事件: protected override CreateParams ...
- rabbitmq 实现延迟队列的两种方式
原文地址:https://blog.csdn.net/u014308482/article/details/53036770 ps: 文章里面延迟队列=延时队列 什么是延迟队列 延迟队列存储的对象肯定 ...
- android 之 启动画面的两种方法
现在,当我们打开任意的一个app时,其中的大部分都会显示一个启动界面,展示本公司的logo和当前的版本,有的则直接把广告放到了上面.启动画面的可以分为两种设置方式:一种是两个Activity实现,和一 ...
- [转]Delphi调用cmd的两种方法
delphi调用cmd的两种方法vars:string;begins:='cmd.exe /c '+edit1.Text+' >c:\1.txt';winexec(pchar(s),sw_hid ...
- 安卓ListView操作的两种方法
举例做一个微信的中间部分(好友消息等信息通知) 第一种:BaseAdapter() package com.example.wx; import java.util.ArrayList;import ...
- Delphi Windows API判断文件共享锁定状态(OpenFile和CreateFile两种方法)
一.概述 锁是操作系统为实现数据共享而提供的一种安全机制,它使得不同的应用程序,不同的计算机之间可以安全有效地共享和交换数据.要保证安全有效地操作共享数据,必须在相应的操作前判断锁的类型,然后才能确定 ...
- 在Delphi中使用C++对象(两种方法,但都要改造C++提供的DLL)
Delphi是市场上最好的RAD工具,但是现在C++占据着主导地位,有时针对一个问题很难找到Delphi或Pascal的解决方案.可是却可能找到了一个相关的C++类.本文描述几种在Delphi代码中使 ...
随机推荐
- 二叉树的建立与遍历(c语言)入门
树其实在本质上就是一对多,链表就是一对一. 二叉树的建立: 这里的代码采用的是最粗暴的创建方法,无实际用处.但初次学习二叉树可以通过这个创建方法更好的理解二叉树. 二叉树的遍历: 遍历在大体上分为递归 ...
- 《逆向工程核心原理》——通过调试方式hook Api
1.附加目标进程, 2.CREATE_PROCESS_DEBUG_EVENT附加事件中将目标api处设置为0xcc(INT 3断点) 3.EXCEPTION_DEBUG_EVENT异常事件中,首先判断 ...
- PTA 统计二叉树叶子结点个数
6-2 统计二叉树叶子结点个数 (10 分) 本题要求实现一个函数,可统计二叉树的叶子结点个数. 函数接口定义: int LeafCount ( BiTree T); T是二叉树树根指针,函数Le ...
- 一文彻底搞懂JS前端5大模块化规范及其区别
码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14577243.html 目录 码文不易,转载请带上本文链接,感谢~ https://www ...
- go的令牌桶实现库 go-rate
关于我 我的博客|文章首发 go-rate是速率限制器库,基于 Token Bucket(令牌桶)算法实现. go-rate被用在LangTrend的生产中 用于遵守GitHub API速率限制. 速 ...
- 全网最值得推荐的ELKB日志学习博客-博客地址留存
博客地址:https://elasticstack.blog.csdn.net/article/details/102728604 博客地址留存,后续解决疑难问题
- Chrome89针对sessionStorage的更新导致数据共享问题
最近将chrome更新到最新的版本,然后发现以前可以正常使用的功能无法使用了,经过分析后发现是浏览器新版本才出现的问题,今天记录以下. 一.遇到的问题 我们具体的问题场景,在A页面中需要打开B页面,同 ...
- 1.设置和验证MySQL数据库的隔离级别
登录数据库 mysql -u root - p 新建用户,各启动一个事物,用于同时操作数据库表中数据 create user tom identified by 'tom'; 然后同时再用该用户启动M ...
- shell脚本 5 sed和awk
文本处理三剑客 在 Shell 下使用这些正则表达式处理文本最多的命令有下面几个工具: 命令 描述 grep 默认不支持扩展表达式,加-E 选项开启 ERE.如果不加-E 使用花括号要加转义符\{\} ...
- 手写Spring DI依赖注入,嘿,你的益达!
目录 提前实例化单例Bean DI分析 DI的实现 构造参数依赖 一:定义分析 二:定义一个类BeanReference 三:BeanDefinition接口及其实现类 四:DefaultBeanFa ...