使用SpringCloud Stream结合rabbitMQ实现消息消费失败重发机制
前言:实际项目中经常遇到消息消费失败了,要进行消息的重发。比如支付消息消费失败后,要分不同时间段进行N次的消息重发提醒。
本文模拟场景
- 当金额少于100时,消息消费成功
- 当金额大于100,小于200时,会进行3次重发,第一次1秒;第二次2秒;第三次3秒。
- 当金额大于200时,消息消费失败,会进行5次重发,第一次1秒;第二次2秒;第三次3秒;第四次4秒;第五次5秒。重试五次后,消息自动进入死信队列,在死信队列存活60秒后消失。
代码实例
特别注意代码与配置文件中的注释,各个使用说明都已经详细写在配置文件中
pom包引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cloudstream</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ①关键配置:引入stream-rabbit 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- ②关键配置:由于stream是基于spring-cloud的,所以这里要引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
配置application.yml文件
注意各个配置的缩进格式,别搞错了
server:
port: 8081
spring:
application:
name: stream-demo
#rabbitmq连接配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: 123456
cloud:
stream:
bindings:
#消息生产者,与DelayDemoTopic接口中的DELAY_DEMO_PRODUCER变量值一致
delay-demo-producer:
#①定义交换机名
destination: demo-delay-queue
#消息消费者,与DelayDemoTopic接口中的DELAY_DEMO_CONSUMER变量值一致
delay-demo-consumer:
#定义交换机名,与①一致,就可以使发送和消费都指向一个队列
destination: demo-delay-queue
#分组,这个配置可以开启消息持久化、可以解决在集群环境下重复消费的问题。
#比如A、B两台服务器集群,如果没有这个配置,则A、B都能收到同样的消息,如果有该配置则只有其中一台会收到消息
group: delay-consumer-group
consumer:
#最大重试次数,默认为3。不使用默认的,这里定义为1,由我们程序控制发送时间和次数
maxAttempts: 1
rabbit:
bindings:
#消息生产者,与DelayDemoTopic接口中的DELAY_DEMO_PRODUCER变量值一致
delay-demo-producer:
producer:
#②申明为延迟队列
delayedExchange: true
#消息消费者,与DelayDemoTopic接口中的DELAY_DEMO_CONSUMER变量值一致
delay-demo-consumer:
consumer:
#申明为延迟队列,与②的配置的成对出现的
delayedExchange: true
#开启死信队列
autoBindDlq: true
#死信队列中消息的存活时间
dlqTtl: 60000
定义队列通道
- 定义通道
/**
* 定义延迟消息通道
*/
public interface DelayDemoTopic {
/**
* 生产者,与yml文件配置对应
*/
String DELAY_DEMO_PRODUCER = "delay-demo-producer";
/**
* 消费者,与yml文件配置对应
*/
String DELAY_DEMO_CONSUMER = "delay-demo-consumer";
/**
* 定义消息消费者,在@StreamListener监听消息的时候用到
* @return
*/
@Input(DELAY_DEMO_CONSUMER)
SubscribableChannel delayDemoConsumer();
/**
* 定义消息发送者,在发送消息的时候用到
* @return
*/
@Output(DELAY_DEMO_PRODUCER)
MessageChannel delayDemoProducer();
}
- 绑定通道
/**
* 配置消息的binding
*
*/
@EnableBinding(value = {DelayDemoTopic.class})
@Component
public class MessageConfig {
}
消息发送模拟
/**
* 发送消息
*/
@RestController
public class SendMessageController {
@Autowired
DelayDemoTopic delayDemoTopic;
@GetMapping("send")
public Boolean sendMessage(BigDecimal money) throws JsonProcessingException {
Message<BigDecimal> message = MessageBuilder.withPayload(money)
//设置消息的延迟时间,首次发送,不设置延迟时间,直接发送
.setHeader(DelayConstant.X_DELAY_HEADER,0)
//设置消息已经重试的次数,首次发送,设置为0
.setHeader(DelayConstant.X_RETRIES_HEADER,0)
.build();
return delayDemoTopic.delayDemoProducer().send(message);
}
}
消息监听处理
@Component
@Slf4j
public class DelayDemoTopicListener {
@Autowired
DelayDemoTopic delayDemoTopic;
/**
* 监听延迟消息通道中的消息
* @param message
*/
@StreamListener(value = DelayDemoTopic.DELAY_DEMO_CONSUMER)
public void listener(Message<BigDecimal> message) {
//获取重试次数
int retries = (int)message.getHeaders().get(DelayConstant.X_RETRIES_HEADER);
//获取消息内容
BigDecimal money = message.getPayload();
try {
String now = DateUtils.formatDate(new Date(),"yyyy-MM-dd HH:mm:ss");
//模拟:如果金额大于200,则消息无法消费成功;金额如果大于100,则重试3次;如果金额小于100,直接消费成功
if (money.compareTo(new BigDecimal(200)) == 1){
throw new RuntimeException(now+":金额超出200,无法交易。");
}else if (money.compareTo(new BigDecimal(100)) == 1 && retries <= 3) {
if (retries == 0) {
throw new RuntimeException(now+":金额超出100,消费失败,将进入重试。");
}else {
throw new RuntimeException(now+":金额超出100,当前第" + retries + "次重试。");
}
}else {
log.info("消息消费成功!");
}
}catch (Exception e) {
log.error(e.getMessage());
if (retries < DelayConstant.X_RETRIES_TOTAL){
//将消息重新塞入队列
MessageBuilder<BigDecimal> messageBuilder = MessageBuilder.fromMessage(message)
//设置消息的延迟时间
.setHeader(DelayConstant.X_DELAY_HEADER,DelayConstant.ruleMap.get(retries + 1))
//设置消息已经重试的次数
.setHeader(DelayConstant.X_RETRIES_HEADER,retries + 1);
Message<BigDecimal> reMessage = messageBuilder.build();
//将消息重新发送到延迟队列中
delayDemoTopic.delayDemoProducer().send(reMessage);
}else {
//超过重试次数,做相关处理(比如保存数据库等操作),如果抛出异常,则会自动进入死信队列
throw new RuntimeException("超过最大重试次数:" + DelayConstant.X_RETRIES_TOTAL);
}
}
}
}
规则定义
目前写在一个常量类里,实际项目中,通常会配置在配置文件中
public class DelayConstant {
/**
* 定义当前重试次数
*/
public static final String X_RETRIES_HEADER = "x-retries";
/**
* 定义延迟消息,固定值,该配置放到消息的header中,会开启延迟队列
*/
public static final String X_DELAY_HEADER = "x-delay";
/**
* 定义最多重试次数
*/
public static final Integer X_RETRIES_TOTAL = 5;
/**
* 定义重试规则,毫秒为单位
*/
public static final Map<Integer,Integer> ruleMap = new HashMap(){{
put(1,1000);
put(2,2000);
put(3,3000);
put(4,4000);
put(5,5000);
}};
}
测试
经过以上配置和实现就可完成模拟的重发场景。
- 浏览器中输入
http://127.0.0.1:8081/send?money=10,可以看到控制台中输出:
消息消费成功!
- 浏览器中输入
http://127.0.0.1:8081/send?money=110,可以看到控制台中输出:
2020-06-20 10:59:42:金额超出100,消费失败,将进入重试。
2020-06-20 10:59:43:金额超出100,当前第1次重试。
2020-06-20 10:59:45:金额超出100,当前第2次重试。
2020-06-20 10:59:48:金额超出100,当前第3次重试。
消息消费成功!
- 浏览器中输入
http://127.0.0.1:8081/send?money=110,可以看到控制台中输出:

注意事项
由于本文用到了延迟队列,需要在rabbitMQ中安装延迟插件,具体安装方式,可以查看:延迟队列安装参考
源码获取
以上示例都可以通过我的GitHub获取完整的代码.
使用SpringCloud Stream结合rabbitMQ实现消息消费失败重发机制的更多相关文章
- SpringCloud stream连接RabbitMQ收发信息
百度上查的大部分都是一些很简单的单消费者或者单生产者的例子,并且多是同一个服务器的配置,本文的例子为多服务器配置下的消费生产和消费者配置. 参考资料:https://docs.spring.io/sp ...
- 九. SpringCloud Stream消息驱动
1. 消息驱动概述 1.1 是什么 在实际应用中有很多消息中间件,比如现在企业里常用的有ActiveMQ.RabbitMQ.RocketMQ.Kafka等,学习所有这些消息中间件无疑需要大量时间经历成 ...
- Spring Cloud Stream消费失败后的处理策略(四):重新入队(RabbitMQ)
应用场景 之前我们已经通过<Spring Cloud Stream消费失败后的处理策略(一):自动重试>一文介绍了Spring Cloud Stream默认的消息重试功能.本文将介绍Rab ...
- Spring Cloud Stream消费失败后的处理策略(三):使用DLQ队列(RabbitMQ)
应用场景 前两天我们已经介绍了两种Spring Cloud Stream对消息失败的处理策略: 自动重试:对于一些因环境原因(如:网络抖动等不稳定因素)引发的问题可以起到比较好的作用,提高消息处理的成 ...
- Spring Cloud Stream消费失败后的处理策略(二):自定义错误处理逻辑
应用场景 上一篇<Spring Cloud Stream消费失败后的处理策略(一):自动重试>介绍了默认就会生效的消息重试功能.对于一些因环境原因.网络抖动等不稳定因素引发的问题可以起到比 ...
- Spring Cloud Stream消费失败后的处理策略(一):自动重试
之前写了几篇关于Spring Cloud Stream使用中的常见问题,比如: 如何处理消息重复消费 如何消费自己生产的消息 下面几天就集中来详细聊聊,当消息消费失败之后该如何处理的几种方式.不过不论 ...
- RockerMQ消息消费、重试
消息中间件—RocketMQ消息消费(一) 消息中间件—RocketMQ消息消费(二)(push模式实现) 消息中间件—RocketMQ消息消费(三)(消息消费重试) MQ中Pull和Push的两种消 ...
- Rabbitmq消费失败死信队列
Rabbitmq 重消费处理 一 处理流程图: 业务交换机:正常接收发送者,发送过来的消息,交换机类型topic AE交换机: 当业务交换机无法根据指定的routingkey去路由到队列的时候,会全部 ...
- RabbitMQ:消息发送确认 与 消息接收确认(ACK)
默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除 如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则 ...
随机推荐
- 洛谷 P1352 没有上司的舞会 树形DP板子
luogu传送门 题目描述: 某大学有n个职员,编号为1~n. 他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司. 现在有个周年庆宴会,宴会每邀请来一个职员都会 ...
- Mysql索引的基本知识和用处
2020-05-29 15:38:27 一.索引的优点 1.能大大减少服务器需要扫描的数据量. 2.帮助服务器避免排序和临时表. 3.将随机io变成顺序io. 二.索引的用处 能够快速匹配where条 ...
- 伪静态%{REQUEST_FILENAME} !-f 和!-d用法
%{REQUEST_FILENAME} !-f 和!-d只对下一条RewriteRule起作用.再往下的RewriteRule不管用. -f 表示为文件 -d 表示为目录 ! 表示非,取反的意思 R ...
- 当微信小程序遇到云开发,再加上一个类似 ColorUI 的模板,人人都能做小程序了
作为一个 Java 程序员,早就想尝试一把微信小程序,但是一直苦于没有想法,再加上做一个漂亮的页面实在不太擅长. 由于自己比较喜欢历史,经常看历史方面的书.在一次梳理中国现有的朝代时,突然想到,要是可 ...
- Java实现 蓝桥杯VIP 算法训练 蜜蜂飞舞
时间限制:1.0s 内存限制:512.0MB 问题描述 "两只小蜜蜂呀,飞在花丛中呀--" 话说这天天上飞舞着两只蜜蜂,它们在跳一种奇怪的舞蹈.用一个空间直角坐标系来描述这个世界, ...
- Java实现 蓝桥杯VIP 算法训练 接水问题
题目描述 有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的一种顺序,使得n个人的平均等待时间最小. 输入输出格式 输入格式: 输入文件共两行,第一行为n:第二行分别 ...
- Java实现 LeetCode 242 有效的字母异位词
242. 有效的字母异位词 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词. 示例 1: 输入: s = "anagram", t = " ...
- Java实现 蓝桥杯VIP 算法提高 去注释
算法提高 去注释 时间限制:1.0s 内存限制:256.0MB 去注释 问题 给你一段C++代码,将其中的注释去除后输出剩余的代码. 注释共有两种形式: 1. 行注视:以//开头,一直作用到行尾为止. ...
- Java实现 洛谷 P1090 合并果子
import java.io.BufferedInputStream; import java.util.Arrays; import java.util.Scanner; public class ...
- Java实现第九届蓝桥杯阶乘位数
阶乘位数 题目描述 小明维护着一个程序员论坛.现在他收集了一份"点赞"日志,日志共有N行.其中每一行的格式是: ts id 表示在ts时刻编号id的帖子收到一个"赞&qu ...