「 从0到1学习微服务SpringCloud 」11 补充篇 RabbitMq实现延迟消费和延迟重试
Mq的使用中,延迟队列是很多业务都需要用到的,最近我也是刚在项目中用到,就在跟大家讲讲吧。
何为延迟队列?
延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。
业务场景
延迟队列能做什么?最常见的是以下两种场景:
- 消费
比如:用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单;用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
- 重试
比如消费者从队列里消费消息时失败了,但是想要延迟一段时间后自动重试。
如果不使用延迟队列,那么我们只能通过一个轮询扫描程序去完成。这种方案既不优雅,也不方便做成统一的服务便于开发人员使用。但是使用延迟队列的话,我们就可以轻而易举地完成。
实现思路
在介绍具体思路钱,先介绍RabbitMQ的两个特性:Time-To-Live Extensions(消息存活时间) 和 Dead Letter Exchanges(死信交换机)
Time-To-Live Extensions
RabbitMQ允许我们为消息或者队列设置TTL(time to live),也就是过期时间。TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter(死信)。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
Dead Letter Exchanges
设置了TTL的消息在过期后会成为Dead Letter。其实在RabbitMQ中,一共有三种消息的“死亡”形式:
消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
消息因为设置了TTL而过期。
消息进入了一条已经达到最大长度的队列。
如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish(推送)到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。
实现流程
延迟消费
延迟消费是延迟队列最为常用的使用模式。如下图所示,生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,这些消息会被设置过期时间,等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。
延迟重试
延迟重试本质上也是延迟消费的一种。
如下图所示,消费者发现该消息处理出现了异常,比如是因为网络波动引起的异常。那么如果不等待一段时间,直接就重试的话,很可能会导致在这期间内一直无法成功,造成一定的资源浪费。那么我们可以将其先放在缓冲队列中(图中红色队列),等消息经过一段的延迟时间后再次进入实际消费队列中(图中蓝色队列),此时由于已经过了“较长”的时间了,异常的一些波动通常已经恢复,这些消息可以被正常地消费。
代码实现
这里只贴上最主要的代码,全部的代码可查看github
1.延迟消费
Mq队列与交换机实例创建
/**
* 缓冲队列
*/
private String DELAY_BUFFER_QUEUE = "delay_buffer_queue";
/**
* 实际消费交换机(DLX)
*/
private String DELAY_SERVICE_EXCHANGE = "delay_service_exchange";
/**
* 实际消费队列
*/
private String DELAY_SERVICE_QUEUE = "delay_service_queue";
/**
* 消息过期时间 3秒
*/
private Integer QUEUE_EXPIRATION = 3 * 1000;
/**
* 实际消费队列
* @return
*/
@Bean
Queue delayServiceQueue(){
return QueueBuilder.durable(DELAY_SERVICE_QUEUE).build();
}
/**
* 实际消费交换机
* @return
*/
@Bean
DirectExchange delayServiceExchange() {
return new DirectExchange(DELAY_SERVICE_EXCHANGE);
}
/**
* 实际消费队列绑定实际消费交换机(DLX)
* @param delayServiceQueue
* @param delayServiceExchange
* @return
*/
@Bean
Binding delayBinding(Queue delayServiceQueue, DirectExchange delayServiceExchange) {
return BindingBuilder.bind(delayServiceQueue)
.to(delayServiceExchange)
.with(DELAY_SERVICE_QUEUE);
}
/**
* 缓冲队列配置
* @return
*/
@Bean
Queue delayBufferQueue(){
return QueueBuilder.durable(DELAY_BUFFER_QUEUE)
// 死信交换机 DLX
.withArgument("x-dead-letter-exchange", DELAY_SERVICE_EXCHANGE)
// 目标routing key
.withArgument("x-dead-letter-routing-key", DELAY_SERVICE_QUEUE)
// 设置队列的过期时间
.withArgument("x-message-ttl", QUEUE_EXPIRATION)
.build();
}
监听实际消费队列
@Component
public class DelayMsgListener {
@RabbitListener(queues="delay_service_queue")
public void listenServiceMsg(Message message){
System.out.println(new Date()+ "收到延迟消息啦:"+new String(message.getBody()));
}
}
测试:发送消息到缓冲队列
@Test
public void send1(){
System.out.println(new Date() +"发送延迟消息!!!");
amqpTemplate.convertAndSend("delay_buffer_queue","Hello!Delay Message!");
}
结果如下
可以看到,在发消息后3秒(TTL),实际消费队列接收到了消息并被消费
2.延迟重试
Mq队列与交换机实例创建
/**
* 缓冲队列
*/
private String RETRY_BUFFER_QUEUE = "retry_buffer_queue";
/**
* 缓冲交换机
*/
private String RETRY_BUFFER_EXCHANGE = "retry_buffer_exchange";
/**
* 实际消费交换机(DLX)
*/
private String RETRY_SERVICE_EXCHANGE = "retry_service_exchange";
/**
* 实际消费队列
*/
private String RETRY_SERVICE_QUEUE = "retry_service_queue";
/**
* 实际消费队列
* @return
*/
@Bean
Queue retryServiceQueue(){
return QueueBuilder.durable(RETRY_SERVICE_QUEUE).build();
}
/**
* 实际消费交换机
* @return
*/
@Bean
DirectExchange retryServiceExchange() {
return new DirectExchange(RETRY_SERVICE_EXCHANGE);
}
/**
* 实际消费队列绑定实际消费交换机(DLX)
* @param retryServiceQueue
* @param retryServiceExchange
* @return
*/
@Bean
Binding retryBinding(Queue retryServiceQueue, DirectExchange retryServiceExchange) {
return BindingBuilder.bind(retryServiceQueue)
.to(retryServiceExchange)
.with(RETRY_SERVICE_QUEUE);
}
/**
* 缓冲队列配置
* @return
*/
@Bean
Queue retryBufferQueue(){
return QueueBuilder.durable(RETRY_BUFFER_QUEUE)
// 死信交换机 DLX
.withArgument("x-dead-letter-exchange", RETRY_SERVICE_EXCHANGE)
// 目标routing key
.withArgument("x-dead-letter-routing-key", RETRY_SERVICE_QUEUE)
// 设置队列的过期时间
.withArgument("x-message-ttl", QUEUE_EXPIRATION)
.build();
}
/**
* 缓冲交换机
* @return
*/
@Bean
DirectExchange retryBufferExchange() {
return new DirectExchange(RETRY_BUFFER_EXCHANGE);
}
/**
* 缓冲队列绑定缓冲交换机
* @param retryBufferQueue
* @param retryBufferQueue
* @return
*/
@Bean
Binding bufferBinding(Queue retryBufferQueue, DirectExchange retryBufferExchange) {
return BindingBuilder.bind(retryBufferQueue)
.to(retryBufferExchange)
.with(RETRY_BUFFER_QUEUE);
}
监听实际消费队列
@Component
public class RetryMsgListener {
/**
* 缓冲队列
*/
private String RETRY_BUFFER_QUEUE = "retry_buffer_queue";
/**
* 缓冲交换机
*/
private String RETRY_BUFFER_EXCHANGE = "retry_buffer_exchange";
@Autowired
private MessagePropertiesConverter messagePropertiesConverter;
@RabbitListener(queues="retry_service_queue")
public void listenServiceMsg(@Payload Message message, Channel channel){
try {
System.out.println(new Date() + "收到消息:" + new String(message.getBody()));
//TODO 业务逻辑
//突然出现异常
throw new RuntimeException("特殊异常");
}catch (Exception e){
Map<String,Object> headers = message.getMessageProperties().getHeaders();
try{
Long retryCount = getRetryCount(headers);
//重试3次
if(retryCount < 3){
retryCount += 1;
System.out.println("消费异常,准备重试,第"+retryCount+"次");
//转换为RabbitMQ 的Message Properties对象
AMQP.BasicProperties rabbitMQProperties =
messagePropertiesConverter.fromMessageProperties( message.getMessageProperties(), "UTF-8");
//设置headers
rabbitMQProperties.builder().headers(headers);
//程序异常重试
//这里必须把rabbitMQProperties也传进来,否则死信队列无法识别是否是同一条信息,导致重试次数无法递增
channel.basicPublish(RETRY_BUFFER_EXCHANGE,RETRY_BUFFER_QUEUE,rabbitMQProperties, message.getBody());
}else {
//TODO 重试失败,需要人工处理 (发送到失败队列或发邮件/信息)
System.out.println("已重试3次,需人工处理!");
}
}catch (IOException ioe){
System.out.println("消息重试失败!");
ioe.printStackTrace();
}
}
}
/**
* 获取重试次数
* 如果这条消息是死信,header中会有一个x-death的记录相关信息
* 其中包含死亡次数
* @param headers
* @return
*/
private long getRetryCount(Map<String, Object> headers) {
long retryCount = 0;
if(null != headers) {
if(headers.containsKey("x-death")) {
List<Map<String, Object>> deathList = (List<Map<String, Object>>) headers.get("x-death");
if(!deathList.isEmpty()) {
Map<String, Object> deathEntry = deathList.get(0);
retryCount = (Long)deathEntry.get("count");
}
}
}
return retryCount;
}
}
测试:发送消息到实际消费队列
@Test
public void send2(){
System.out.println(new Date() +"发送延迟重试消息!!!");
//直接发消息到实际消费队列
amqpTemplate.convertAndSend("retry_service_queue","Hello!Retry Message!");
}
结果如下:
可以看到,消费异常后,重试了3次
延迟队列在实际业务中是经常被用到的,同学们最好都学学哦,代码已上传github
https://github.com/zhangwenkang0/springcloud-learning-from-0-to-1/tree/master/rabbitmq-demo
如果觉得不错,分享给你的朋友!
一个立志成大腿而每天努力奋斗的年轻人
伴学习伴成长,成长之路你并不孤单!
「 从0到1学习微服务SpringCloud 」11 补充篇 RabbitMq实现延迟消费和延迟重试的更多相关文章
- 「 从0到1学习微服务SpringCloud 」09 补充篇-maven父子模块项目
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」06 统一配置中心Spring Cloud Config 「 从0到1学习微服务SpringCloud 」07 RabbitM ...
- 「 从0到1学习微服务SpringCloud 」10 服务网关Zuul
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」06 统一配置中心Spring Cloud Config 「 从0到1学习微服务SpringCloud 」07 RabbitM ...
- 「 从0到1学习微服务SpringCloud 」08 构建消息驱动微服务的框架 Spring Cloud Stream
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- 「 从0到1学习微服务SpringCloud 」07 RabbitMq的基本使用
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- 「 从0到1学习微服务SpringCloud 」06 统一配置中心Spring Cloud Config
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- 「 从0到1学习微服务SpringCloud 」05服务消费者Fegin
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- 「 从0到1学习微服务SpringCloud 」04服务消费者Ribbon+RestTemplate
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- 「 从0到1学习微服务SpringCloud 」03 Eureka的自我保护机制
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 Eureka的高可用需要 ...
- 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! Spring Cloud Eureka 基于Netflix Eureka做了二次封装(Spring Clo ...
随机推荐
- Linux 内核设备驱动
设备模型跟踪所有对系统已知的驱动. 这个跟踪的主要原因是使驱动核心能匹配驱动和新 设备. 一旦驱动在系统中是已知的对象, 但是, 许多其他的事情变得有可能. 设备驱动可 输出和任何特定设备无关的信息和 ...
- CodeForces 1204 (#581 div 2)
传送门 A.BowWow and the Timetable •题意 给你一个二进制数,让你求小于这个数的所有4的幂的个数 •思路 第一反应是二进制与四进制转换 (其实不用真正的转换 QwQ) 由于二 ...
- SPOJ VLATTICE (莫比乌斯反演)
传送门:https://www.spoj.com/problems/VLATTICE/en/ 题意: 在三维坐标系下,你在点(0,0,0),看的范围是(n,n,n)以内,求你可以看见多少个点没有被遮挡 ...
- mysql find_in_set 与 in 的用法与区别,mysql范围搜索,mysql范围查询
mysql find_in_set 与 in 的用法与区别 1.find_in_set 用于模糊查询,并且数据库中的值是用英文逗号分隔的: 例如: (1).去字段中查询 select find_in_ ...
- HDU 2899 Strange fuction [二分]
1.题意:给一个函数F(X)的表达式,求其最值,自变量定义域为0到100 2.分析:写出题面函数的导函数的表达式,二分求导函数的零点,对应的就是极值点 3.代码: # include <iost ...
- Excel基本功能
公式基础: 比较运算符的种类 flase对应0 而ture对应1 连接运算 利用之前提到的ture就是1 乘以100 注意用括号区分优先级 函数应用基础: 系统已经列好这几个常用的函数 右键单击状态栏 ...
- 如何看Crash 文件
如何查看崩溃日志 好了,获得是人类可读语言的崩溃日志后,或者是从别人手机到处崩溃日志后,下一步就是查看了.下面就正对一个程序猿该如何看稍微说说. 崩溃日志头 1 2 3 4 5 6 7 8 9 ...
- Centos 7.5安装 Nginx 1.14.1
1. 准备工作 查看系统版本 输入命令 cat /etc/redhat-release 我的Centos版本 CentOS Linux release 7.5.1804 (Core) 安装nginx所 ...
- JAVA CONCURRENT FRAMEWORK
1.ConcurrentHashMap 在并发中使用hashmap容易导致死锁,hashmap存在以下问题 1.线程不安全的hashmap 在多线程环境下使用hashmapPut操作时会引起死循环,因 ...
- $HDU1848\ Fibonacci\ again\ and\ again$ 博弈论
正解:博弈论 解题报告: 传送门! 首先按照套路显然是考虑先预处理出所有数的$SG$函数值然后全局的$SG$就是$SG(n)$^$SG(m)$^$SG(p)$,这儿应该麻油问题$QwQ$? 然后就考虑 ...