spring boot整合RabbitMQ详解;消息的确认机制,发送确认(ConfirmCallback, ReturnsCallback),消费手动确认(ACK)
简介
什么叫消息队列?
消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
消息队列的应用场景
可以看出消息队列是一种应用间的异步协作机制,那什么时候需要使用 MQ 呢?
以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。
以上是用于业务解耦的情况,其它常见场景包括最终一致性、广播、错峰流控等等。
RabbitMQ 特点
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
RabbitMQ 基本概念
RabbitMQ 内部结构
1、Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。
Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。
headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了
下面的代码中也不会编写 headers类型的代码
Direct Exchange (直连型交换机)
直连型交换机,根据消息携带的路由键将消息投递给对应队列。
大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
Fanout Exchange(扇型交换机)
扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
Topic Exchange(主题交换机)
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:
* (星号) 用来表示一个单词 (必须出现的)
# (井号) 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 *.TT.* 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
RabbitMQ安装
一、这是RabbitMQ的官网地址(https://www.rabbitmq.com/install-windows.html) 可以按照官网的文档进行安装
ps:看了下官方文档rabbitMQ是用Erlang 语言开发的,所以需要先安装Erlang 语言。
二、docker-compose安装
建议通过docker进行安装,毕竟用docker命令安装很方便,下面就是我用docker-compose安装rabbitmq的yml文件配置:
version: '3'
services:
my_rabbitMQ:
image: "rabbitmq:3.8.3-management"
container_name: my_rabbitMQ
restart: always
privileged: true
ports:
- "15672:15672"
- "5672:5672"
environment:
- "RABBITMQ_DEFAULT_USER=rabbitMQ"
- "RABBITMQ_DEFAULT_PASS=rabbitMQ"
docker-compose 安装rabbitMQ命令
docker-compose -f rabbitMq-docker-compose.yml up -d
1、docker-compose安装成功后
2、浏览器访问 http://127.0.0.1:15672/ ,并输入账号/密码 rabbitMQ/rabbitMQ 能正常访问rabbitMQ管理界面,就代表安装完成。
spring boot集成RabbitMQ(编码)
项目有两个rabbit-provider(生产者)和rabbit-consumer(消费者)
集成rabbitMQ 主要需要依赖spring-boot-starter-amqp;
java-testdata-generator 是我在gitee上看到 随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号,电子邮箱地址和生成insert sql参数列表字符串等的工具包。主要是用来不让测试数据看起来那么单调;
maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.4</version>
</dependency>
<!--测试接口 添加swagger start-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<!--测试接口 添加swagger end-->
<!--Java实现的各种随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号 start-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>java-testdata-generator</artifactId>
<version>1.1.2</version>
</dependency>
<!--Java实现的各种随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号 end-->
application.yml配置
server:
port: 8999
spring:
#项目名称
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: rabbitMQ
password: rabbitMQ
# #虚拟host 可以不设置,使用server默认host
# virtual-host: xkq
#确认消息已发送到交换机(Exchange)
# publisher-confirm-type: SIMPLE
publisher-confirm-type: CORRELATED
#确认消息已发送到队列(Queue)
publisher-returns: true
Direct Exchange (直连型交换机)
项目rabbit-provider(生产者)
DirectRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectRabbitConfig {
public static final String TestDirectQueue = "TestDirectQueue";
public static final String TestDirectExchange = "TestDirectExchange";
public static final String TestDirectRouting = "TestDirectRouting";
//队列 起名:TestDirectQueue
@Bean
public Queue TestDirectQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue(TestDirectQueue,true);
}
//Direct交换机 起名:TestDirectExchange
@Bean
DirectExchange TestDirectExchange() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange(TestDirectExchange,true,false);
}
//绑定 将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(TestDirectQueue())
.to(TestDirectExchange()).with(TestDirectRouting);
}
}
SendDirectMessageController业务发送消息
import cn.binarywang.tools.generator.ChineseAddressGenerator;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.example.rabbitmqprovider.direct.config.DirectRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
public class SendDirectMessageController {
@Autowired
RabbitTemplate rabbitTemplate; //使用RabbitTemplate,这提供了接收/发送等等方法
@ResponseBody
@GetMapping("/sendDirectMessage")
public Object sendDirectMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
ChineseAddressGenerator.getInstance()
.generate());
Map<String,Object> map = new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime", DateUtil.now());
//将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange, DirectRabbitConfig.TestDirectRouting, map);
return map;
}
}
项目rabbit-consumer(消费者)
DirectReceiver接收消息
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class DirectReceiver {
public static final String TestDirectQueue = "TestDirectQueue";
public static final String TestDirectExchange = "TestDirectExchange";
public static final String TestDirectRouting = "TestDirectRouting";
@RabbitListener(queues = DirectReceiver.TestDirectQueue)
@RabbitHandler
public void receiver(@Payload HashMap message) {
log.info("receiver 消费者1号 收到消息 --- message:{}" ,message);
}
@RabbitListener(queues = TestDirectQueue)
@RabbitHandler
public void receiver2(Map testMessage) {
log.info("receiver2 消费者2号 收到消息 getClass:{} --- {}" ,testMessage.getClass(), testMessage);
}
}
测试Direct Exchange (直连型交换机)
正常调用接口【http://127.0.0.1:8999/sendDirectMessage 】 ;并且rabbitMQ管理界面看到TestDirectQueue队列有消息新增,代表消息发送成功;
看到控制台日志输出如下,代表消费者 成功接收到推送的消息;多个消费者的情况下 默认是采用轮询的方式进行消费。
Fanout Exchange(扇型交换机)
项目rabbit-provider(生产者)
FanoutRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutRabbitConfig {
public final static String fanoutExchange = "fanoutExchange";
public static final String fanout_A = "fanout.A";
public static final String fanout_B = "fanout.B";
public static final String fanout_C = "fanout.C";
/**
* 创建三个队列 :fanout.A fanout.B fanout.C
* 将三个队列都绑定在交换机 fanoutExchange 上
* 因为是扇型交换机, 路由键无需配置,配置也不起作用
*/
@Bean
public Queue queueA() {
return new Queue(fanout_A);
}
@Bean
public Queue queueB() {
return new Queue(fanout_B);
}
@Bean
public Queue queueC() {
return new Queue(fanout_C);
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(fanoutExchange);
}
@Bean
Binding bindingExchangeA() {
return BindingBuilder.bind(queueA()).to(fanoutExchange());
}
@Bean
Binding bindingExchangeB() {
return BindingBuilder.bind(queueB()).to(fanoutExchange());
}
@Bean
Binding bindingExchangeC() {
return BindingBuilder.bind(queueC()).to(fanoutExchange());
}
}
SendFanoutMessageController业务发送消息
import cn.hutool.core.date.DateUtil;
import com.example.rabbitmqprovider.direct.config.FanoutRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。
* 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
*/
@RestController
public class SendFanoutMessageController {
@Autowired
RabbitTemplate rabbitTemplate; //使用RabbitTemplate,这提供了接收/发送等等方法
@GetMapping("/sendFanoutMessage")
public String sendFanoutMessage() {
String messageData = "FanoutMessage routingKey is null";
Map<String, Object> map = new HashMap<>();
map.put("createTime", DateUtil.now());
map.put("messageData", messageData);
rabbitTemplate.convertAndSend(FanoutRabbitConfig.fanoutExchange,null, map);
return "ok";
}
@GetMapping("/sendFanoutMessage1")
public String sendFanoutMessage1() {
String messageData = "FanoutMessage routingKey is 'xxx'";
Map<String, Object> map = new HashMap<>();
map.put("createTime", DateUtil.now());
map.put("messageData", messageData);
//扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个接口绑定一下路由键
rabbitTemplate.convertAndSend(FanoutRabbitConfig.fanoutExchange,"xxx", map);
return "ok";
}
}
项目rabbit-consumer(消费者)
FanoutReceiver接收消息
import java.util.Map;
@Slf4j
@Component
public class FanoutReceiver {
public static final String fanout_A = "fanout.A";
public static final String fanout_B = "fanout.B";
public static final String fanout_C = "fanout.C";
@RabbitListener(queues = fanout_A)
@RabbitHandler
public void fanout_A(Map testMessage) {
log.info("fanout_A {}" , testMessage);
}
@RabbitListener(queues = fanout_B)
@RabbitHandler
public void fanout_B(Map testMessage) {
log.info("fanout_B {}" , testMessage);
}
@RabbitListener(queues = fanout_C)
@RabbitHandler
public void fanout_C(Map testMessage) {
log.info("fanout_C {}" , testMessage);
}
}
测试 Fanout Exchange(扇型交换机)
先调用http://127.0.0.1:8999/sendFanoutMessage ,在调用接口 http://127.0.0.1:8999/sendFanoutMessage1
日志输出如下,可以看到 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。
这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
Topic Exchange(主题交换机)
项目rabbit-provider(生产者)
TopicRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TopicRabbitConfig {
//绑定键
public final static String man = "topic.man";
public final static String woman = "topic.woman";
public final static String xxx = "xxx";
public final static String topicExchange = "topicExchange";
@Bean
public Queue firstQueue() {
return new Queue(TopicRabbitConfig.man);
}
@Bean
public Queue secondQueue() {
return new Queue(TopicRabbitConfig.woman);
}
@Bean
public Queue thirdQueue() {
return new Queue(TopicRabbitConfig.xxx);
}
@Bean
TopicExchange exchange() {
return new TopicExchange(topicExchange);
}
//将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
//这样只要是消息携带的路由键是topic.man,才会分发到该队列
@Bean
Binding bindingExchangeMessage() {
return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
}
//将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
// 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
@Bean
Binding bindingExchangeMessage2() {
return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
}
@Bean
Binding bindingExchangeMessage3() {
return BindingBuilder.bind(thirdQueue()).to(exchange()).with("#");
}
}
SendTopicMessageController业务发送消息
import com.example.rabbitmqprovider.direct.config.TopicRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SendTopicMessageController {
@Autowired
RabbitTemplate rabbitTemplate; //使用RabbitTemplate,这提供了接收/发送等等方法
@ResponseBody
@GetMapping("/sendTopicMessage1")
public Object sendTopicMessage1() {
String messageData = "message: M A N ";
Map<String, Object> manMap = new HashMap<>();
manMap.put("messageData", messageData);
rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "topic.man", manMap);
return "ok";
}
@ResponseBody
@GetMapping("/sendTopicMessage2")
public Object sendTopicMessage2() {
String messageData = "message: woman is all ";
Map<String, Object> womanMap = new HashMap<>();
womanMap.put("messageData", messageData);
rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "topic.woman", womanMap);
return "ok";
}
@ResponseBody
@GetMapping("/sendTopicMessage3")
public Object sendTopicMessage3() {
String messageData = "message: xxx ";
Map<String, Object> womanMap = new HashMap<>();
womanMap.put("messageData", messageData);
//routingKey 设置'abc';xxx队列 routingKey配置了# 看能否收到消息
rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "abc", womanMap);
return "ok";
}
}
项目rabbit-consumer(消费者)
TopicReceiver接收消息
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
public class TopicReceiver {
public static final String topic_man = "topic.man";
public static final String topic_woman = "topic.woman";
public final static String xxx = "xxx";
@RabbitListener(queues = topic_man)
@RabbitHandler
public void topic_man(Map testMessage) {
log.info("我是队列{} 收到消息:{}" ,topic_man, testMessage);
}
@RabbitListener(queues = topic_woman)
@RabbitHandler
public void topic_woman(Map testMessage) {
log.info("我是队列{} 收到消息:{}" ,topic_woman, testMessage);
}
@RabbitListener(queues = xxx)
@RabbitHandler
public void xxx(Map testMessage) {
log.info("我是队列{} 收到消息:{}" ,xxx, testMessage);
}
}
测试Topic Exchange(主题交换机)
- 调用sendTopicMessage1接口
routingKey设置的是"topic.man",所以三个队列都收到消息;
- 调用sendTopicMessage2接口
routingKey设置的是"topic.woman",“topic.man”队列设置的routingKey是“topic.man”所以没收到消息;
- 调用sendTopicMessage3接口
routingKey设置的是"abc",队列“xxx”配置的routingKey是“#”,所以只有队列“xxx”收到了消息
消息可靠性
rabbitmq 的消息确认分为两部分:发送消息确认 和 消息接收确认。
项目rabbit-provider(生产者)消息发送确认
发送消息确认:用来确认生产者 producer 将消息发送到 broker ,broker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。
消息从 producer 到 rabbitmq broker有一个 confirmCallback 确认模式。
消息从 exchange 到 queue 投递失败有一个 returnCallback 退回模式。
我们可以利用这两个Callback来确保消的100%送达。
1、 ConfirmCallback确认模式
- ConfirmCallback机制只确认消息是否到达exchange(交换器),不保证消息可以路由到正确的queue;
- 配置参数需要设置:publisher-confirm-type: CORRELATED;springboot版本较低的话参数设置改成:publisher-confirms: true
2、 ReturnCallback 退回模式
-
ReturnsCallback 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息
-
配置参数需要设置:publisher-returns: true
-
配置参数在application.yml文件(publisher-confirm-type、publisher-returns)
消息发送确认配置如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
// Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者,为false时匹配不到会直接被丢弃
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* ConfirmCallback机制只确认消息是否到达exchange(交换器),不保证消息可以路由到正确的queue;
* 需要设置:publisher-confirm-type: CORRELATED;
* springboot版本较低 参数设置改成:publisher-confirms: true
*
* 以实现方法confirm中ack属性为标准,true到达
* config : 需要开启rabbitmq得ack publisher-confirm-type
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("ConfirmCallback 确认结果 (true代表发送成功) : {} 消息唯一标识 : {} 失败原因 :{}",ack,correlationData,cause);
}
});
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* ReturnsCallback 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息
* 就需要这种return机制
*
* config : 需要开启rabbitmq发送失败回退; publisher-returns 或rabbitTemplate.setMandatory(true); 设置为true
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
// 实现接口ReturnCallback,重写 returnedMessage() 方法,
// 方法有五个参数
// message(消息体)、
// replyCode(响应code)、
// replyText(响应内容)、
// exchange(交换机)、
// routingKey(队列)。
log.info("ReturnsCallback returned : {}",returned);
}
});
return rabbitTemplate;
}
}
消息发送确认测试接口:
import cn.binarywang.tools.generator.ChineseAddressGenerator;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.util.StrUtil;
import com.example.rabbitmqprovider.direct.config.DirectRabbitConfig;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
public class SendCallbackMessageController {
@Autowired
RabbitTemplate rabbitTemplate; //使用RabbitTemplate,这提供了接收/发送等等方法
@ResponseBody
@GetMapping("/sendMessageToExchangeFail")
public Object sendMessageToExchangeFail() {
String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
ChineseAddressGenerator.getInstance()
.generate());
Map<String,Object> map = new HashMap<>();
map.put("messageData",messageData);
//发送一个消息到 不存在到exchange
rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange.concat("test"), DirectRabbitConfig.TestDirectRouting, map,new CorrelationData(UUID.randomUUID().toString()));
return map;
}
@ResponseBody
@GetMapping("/sendMessageToQueueFail")
public Object sendMessageToQueueFail() {
String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
ChineseAddressGenerator.getInstance()
.generate());
Map<String,Object> map = new HashMap<>();
map.put("messageData",messageData);
//发送一个消息到 不存的队列里;
rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange, "xxx", map,new CorrelationData(UUID.randomUUID().toString()));
return map;
}
}
消息发送确认测试
- 调用sendMessageToExchangeFail接口
2022-03-06 20:31:00.488 ERROR 32087 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'TestDirectExchangetest' in vhost '/', class-id=60, method-id=40)
2022-03-06 20:31:00.489 INFO 32087 --- [nectionFactory2] c.e.r.direct.config.RabbitConfig : ConfirmCallback 确认结果 (true代表发送成功) : false 消息唯一标识 : CorrelationData [id=5aaf1d44-85cc-42ae-8ae5-cbca5074cefd] 失败原因 :channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'TestDirectExchangetest' in vhost '/', class-id=60, method-id=40)
- 调用sendMessageToQueueFail接口
2022-03-06 20:31:30.186 INFO 32087 --- [nectionFactory2] c.e.r.direct.config.RabbitConfig : ReturnsCallback returned : ReturnedMessage [message=(Body:'[serialized object]' MessageProperties [headers={spring_returned_message_correlation=8699b6b8-0b12-420f-b866-73f54ba0a002}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]), replyCode=312, replyText=NO_ROUTE, exchange=TestDirectExchange, routingKey=xxx]
2022-03-06 20:31:30.186 INFO 32087 --- [nectionFactory1] c.e.r.direct.config.RabbitConfig : ConfirmCallback 确认结果 (true代表发送成功) : true 消息唯一标识 : CorrelationData [id=8699b6b8-0b12-420f-b866-73f54ba0a002] 失败原因 :null
项目rabbit-consumer(消费者)消息接收确认
消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
消息确认模式有:
- AcknowledgeMode.NONE:自动确认
- AcknowledgeMode.AUTO:根据情况确认(默认值)
- AcknowledgeMode.MANUAL:手动确认
手动确认消息
1、basicAck
表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
void basicAck(long deliveryTag, boolean multiple)
-
deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
-
multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。
-
举个栗子: 假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。
2、basicNack
表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
- deliveryTag:表示消息投递序号。
- multiple:是否批量确认。
- requeue:值为 true 消息将重新入队列。
3、basicReject
拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。
void basicReject(long deliveryTag, boolean requeue)
- deliveryTag:表示消息投递序号。
- requeue:值为 true 消息将重新入队列。
开启手动确认消息
- application.yml配置文件开启
acknowledge-mode设置manual
spring:
#项目名称
application:
name: rabbitmq-custom
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: rabbitMQ
password: rabbitMQ
listener:
simple:
acknowledge-mode: manual
- 注解开启手动确认
@RabbitListener注解中设置参数ackMode= "MANUAL"开启
import com.rabbitmq.client.Channel;
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.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
/**
* 先注释 DirectReceiver的@Component
* 用TestAckDirectReceiver方法来验证 手动ack
*/
@Slf4j
@Component
public class TestAckDirectReceiver {
public static final String TestDirectQueue = "TestDirectQueue";
public static final String TestDirectExchange = "TestDirectExchange";
public static final String TestDirectRouting = "TestDirectRouting";
@RabbitListener(queues = TestAckDirectReceiver.TestDirectQueue,
ackMode= "MANUAL")
@RabbitHandler
public void receiver(@Payload HashMap dataMsg, Channel channel, Message message) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.info("消费者1号 deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
try {
int i = 1/0;
} catch (Exception e) {
log.error("MyAckReceiver error : {} deliveryTag:{}",e.getMessage(),deliveryTag);
channel.basicReject(deliveryTag, true);
}
}
@RabbitListener(queues = TestAckDirectReceiver.TestDirectQueue,
ackMode= "MANUAL")
@RabbitHandler
public void receiver1(@Payload HashMap dataMsg, Channel channel, Message message) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.info("消费者2号 deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
channel.basicAck(deliveryTag,true);
}
}
手动消息确认消费测试
可以看到消费者1号调用basicReject方法拒绝了消息;消费者2号调用channel.basicAck方法手动确认了消费
2022-03-06 21:23:16.816 INFO 51530 --- [ntContainer#4-1] c.e.c.direct.TestAckDirectReceiver : 消费者1号 deliveryTag:5 dataMsg:{createTime=2022-03-06 21:23:16, messageId=33ae8439-4635-4499-ae7d-c167f100c26a, messageData=hello 我叫:毛儿 我住在:湖南省湘西土家族苗族自治州露劲路7725号纯辟顽小区8单元1122室}
2022-03-06 21:23:16.816 ERROR 51530 --- [ntContainer#4-1] c.e.c.direct.TestAckDirectReceiver : MyAckReceiver error : / by zero deliveryTag:5
2022-03-06 21:23:16.818 INFO 51530 --- [ntContainer#3-1] c.e.c.direct.TestAckDirectReceiver : 消费者2号 deliveryTag:6 dataMsg:{createTime=2022-03-06 21:23:16, messageId=33ae8439-4635-4499-ae7d-c167f100c26a, messageData=hello 我叫:毛儿 我住在:湖南省湘西土家族苗族自治州露劲路7725号纯辟顽小区8单元1122室}
代码我已经上传到gitee 代码传送门
参考资料:
https://www.jianshu.com/p/79ca08116d57
https://blog.csdn.net/qq_35387940/article/details/100514134
https://blog.csdn.net/weixin_32820639/article/details/111240447
https://www.cnblogs.com/gyjx2016/p/13705307.html
https://zhuanlan.zhihu.com/p/152325703
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 一个费力不讨好的项目,让我损失了近一半的绩效!