SpringBoot教程(十五) | SpringBoot集成RabbitMq
转载
一缕82年的清风
大佬的文章(仅仅供个人学习用):
SpringBoot教程(十五) | SpringBoot集成RabbitMq
(一)rabbitMq是什么
RabbitMq是我们在开发过程中经常会使用的一种消息队列。
RabbitMQ是一个开源的消息代理和队列服务器,用来实现各个应用服务间的数据共享(跨平台 ,跨语言)。
RabbitMQ是使用erlang语言编写的,并且基于AMQP协议实现。
所有的消息队列产品模型抽象上来说,都是类似的过程。
生产者创建消息,然后发布到消息队列中,由消费者进行消费。
(二)rabbitMq的后台管理界面
安装完rabbitMq后,输入http://ip:15672/ ,是可以看到一个简单后台管理界面的。
在这个界面里面我们可以做些什么?
可以手动创建虚拟host,创建用户,分配权限,创建交换机,创建队列等等,还有查看队列消息,消费效率,推送效率等等。
以上这些管理界面的操作在这篇暂时不做扩展描述,我想着重介绍后面实例里会使用到的。
(三)消息推送的简述
首先先介绍一个简单的一个消息推送到接收的流程,提供一个简单的图:
黄色的圈圈就是我们的消息推送服务,将消息推送到 中间方框里面也就是 rabbitMq的服务器,
然后经过服务器里面的交换机、队列等各种关系(后面会详细讲)将数据处理入列后,
最终右边的蓝色圈圈消费者获取对应监听的消息。
Exchange分发消息的时候根据类型的不同分发策略有所区别,
目前常见的有四种类型: direct(直连型交换机)、fanout(扇型交换机)、topic(主题交换机)、headers(标头交换机)。
headers匹配AMQP消息的是 header而不是 路由键,
此外headers交换机和direct交换机完全一致但是性能差很多,
几乎用不到了,所以直接看另外三种类型
2.1 direct 直连型交换机(RabbitMq默认的交换机类型,也是最常用的)
Binding 的含义:
绑定, 用于交换机和消息列队之间的关联。
一个绑定就是基于路由键(routing-key)将交换机和消息队列连接起来的路由规则。
所以可以将交换机理解成一个由绑定构成的路由表。
BindingBuilder的构成
Binding的由 队列(Queue)、交换机(Exchange)和路由键(Routing Key)相关联构成
消息中的路由键(routing key)如果和Binding中的bing key一致,交换机就将消息发送到队列的队列中。路由键要完全匹配,单个传播
。
简述: 直连型交换机发送消息 根据 消息中的路由键 找到Binding 在获取Binding里面的队列,然后再存到这个队列中去。
2.2 fanout 扇型交换机
每个发到fanout扇型交换机的消息都会分到所有绑定的队列上去。
fanout交换器扇型交换机不处理路由键,只是简单的将队列绑定到交换机上,每个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。
很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的
。
2.3 topic 主题交换机
topic主题交换机通过模式匹配分配路由的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。
它将路由键和绑定键的字符串切分成单词,这些单词之间用.隔开。它同样会识别两个通配符: # 和* 。 #匹配0个或多个单词, * 匹配一个单词
(四)springBoot集成RabbitMQ
SpringBoot集成rabbitMQ还是比较简单的,因为springBoot使用RabbitTemplate对常用操作进行了封装。
接下来我们来看一下集成过程。首先导入依赖。
<!-- 无需在parent的配置文件中添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
然后在springBoot的配置文件 application.yml中配置rabbitMQ连接信息
server:
port: 7890
spring:
rabbitmq:
host: 172.15.33.52
port: 5672
username: root
password: 123456
接下来我们我们分三种交换机进行演示。
3.1 direct 直连型交换机
(一) 创建配置类
首先是配置类,在配置类中我们需要声明交换机,队列和绑定关系。
@Configuration
public class DirectExchangeConfig {
//定义队列的名称常量
public static final String DIRECT_QUEUE = "directQueue";
public static final String DIRECT_QUEUE2 = "directQueue2";
//定义直接交换机的名称常量
public static final String DIRECT_EXCHANGE = "directExchange";
//定义路由键常量,用于交换机和队列之间的绑定
public static final String DIRECT_ROUTING_KEY = "direct";
//定义队列,名称为DIRECT_QUEUE
@Bean
public Queue directQueue() {
return new Queue(DIRECT_QUEUE, true);
}
//定义队列,名称为DIRECT_QUEUE2
@Bean
public Queue directQueue2() {
return new Queue(DIRECT_QUEUE2, true);
}
//定义直接交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange(DIRECT_EXCHANGE, true, false);
}
//定义一个绑定,将directQueue队列绑定到directExchange交换机上,
//使用direct作为路由键
@Bean
public Binding bindingDirectExchange(Queue directQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue).to(directExchange).with(DIRECT_ROUTING_KEY);
}
// 定义一个绑定Bean,将directQueue2队列也绑定到directExchange交换机上,
// 同样使用direct作为路由键
@Bean
public Binding bindingDirectExchange2(Queue directQueue2, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue2).to(directExchange).with(DIRECT_ROUTING_KEY);
}
}
这里我们创建了一个叫directExchange的交换机,绑定了directQueue和directQueue2两个队列,路由键都为是direct.
这意味着发送到directExchange交换机且路由键为direct的消息将同时被directQueue和directQueue2接收
(二)消息生产者
消息的生产者,我们通过一个Controller来进行模拟,直接引用rabbitTemplate
@RestController
@Slf4j
@RequestMapping("/direct")
public class DirectController {
private final RabbitTemplate rabbitTemplate;
public DirectController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
/**
* direct交换机为直连模式交换机
* 根据消息携带的路由键将消息投递给对应队列
*
*
* @return
*/
@GetMapping("send")
public Object sendMsg() {
rabbitTemplate.convertAndSend(DirectExchangeConfig.DIRECT_EXCHANGE, DirectExchangeConfig.DIRECT_ROUTING_KEY, "发送一条测试消息:direct");
return "direct消息发送成功!!";
}
}
当我在浏览器访问对应连接的时候,就会生产一条消息发送到directExchange交换机,路由key为:direct, 消息内容为:发送一条测试消息:direct
(三)消息消费者
接下来我们来看消息的消费者。
package com.lsqingfeng.action.rabbitmq.direct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @className: DirectQueueListener
* @description: 直连交换机的监听器
* @author: sh.Liu
* @date: 2021-08-23 16:03
*/
@Slf4j
@Component
public class DirectQueueListener {
/**
* 尽管设置了两个消费者,但是只有一个能够消费成功
* 多次发送则轮训消费:
* DirectReceiver消费者收到消息1 : 发送一条测试消息:direct
* DirectReceiver消费者收到消息2 : 发送一条测试消息:direct
* DirectReceiver消费者收到消息1 : 发送一条测试消息:direct
* DirectReceiver消费者收到消息2 : 发送一条测试消息:direct
*
* 一个交换机可以绑定多个队列。如果通过路由key可以匹配到多个队列,消费的时候也只能有一个进行消费
* @param testMessage
*/
@RabbitHandler
@RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE)
public void process(String testMessage) {
System.out.println("DirectReceiver消费者收到消息1 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE)
public void process2(String testMessage) {
System.out.println("DirectReceiver消费者收到消息2 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = DirectExchangeConfig.DIRECT_QUEUE2)
public void process3(String testMessage) {
System.out.println("DirectReceiver消费者收到消息3 : " + testMessage);
}
}
当我们访问浏览器生产消息时,可以观察控制台结果:
DirectReceiver消费者收到消息1 : 发送一条测试消息:direct
DirectReceiver消费者收到消息3 : 发送一条测试消息:direct
再发送一次:
DirectReceiver消费者收到消息3 : 发送一条测试消息:direct
DirectReceiver消费者收到消息2 : 发送一条测试消息:direct
由于我们又两个队列都绑定了交换机,且routeKey一样,所以会打印两条。
要注意direct只有routeKey完全匹配的时候才能被消费,
同时每个队列中的消息 只会 被消费一次
。
3.2 fanout 扇型交换机
(一) 创建配置类
@Configuration
public class FanoutExchangeConfig {
public static final String FANOUT_QUEUE = "fanoutQueue";
public static final String FANOUT_QUEUE2 = "fanoutQueue2";
public static final String FANOUT_QUEUE3 = "fanoutQueue3";
public static final String FANOUT_EXCHANGE = "fanoutExchange";
public static final String FANOUT_ROUTING_KEY = "fanout";
@Bean
public Queue fanoutQueue() {
return new Queue(FANOUT_QUEUE, true);
}
@Bean
public Queue fanoutQueue2() {
return new Queue(FANOUT_QUEUE2, true);
}
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE, true, false);
}
@Bean
public Binding bindingFanoutExchange(Queue fanoutQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}
@Bean
public Binding bindingFanoutExchange2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
这里也是用一个Fanout类型的交换机绑定了两个队列,要注意在这种模式下,是不需要指定routing-Key的,因为所有绑定的队列都会收到消息。
(二)消息生产者
生产者代码如下:
@RestController
@Slf4j
@RequestMapping("/fanout")
public class FanoutController {
private final RabbitTemplate rabbitTemplate;
public FanoutController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
/**
* fanout交换机为扇形模式交换机
* 消息会发送到所有绑定的队列上。
* @return
*/
@GetMapping("send")
public Object sendMsg() {
rabbitTemplate.convertAndSend(FanoutExchangeConfig.FANOUT_EXCHANGE, null, "发送一条测试消息:fanout");
return "fanout消息发送成功!!";
}
}
(三)消息消费者
消息的消费者:
@Slf4j
@Component
public class FanoutQueueListener {
/**
* fanout交换机: 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列
* 同一个队列监听多次,只会消费一次。
* 交换机绑定的多个队列都可以收到消息
* @param testMessage
*/
@RabbitHandler
@RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE)
public void process(String testMessage) {
System.out.println("FanoutReceiver消费者收到消息1 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE)
public void process2(String testMessage) {
System.out.println("FanoutReceiver消费者收到消息2 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = FanoutExchangeConfig.FANOUT_QUEUE2)
public void process3(String testMessage) {
System.out.println("FanoutReceiver消费者收到消息3 : " + testMessage);
}
}
打印结果:
FanoutReceiver消费者收到消息1 : 发送一条测试消息:fanout
FanoutReceiver消费者收到消息3 : 发送一条测试消息:fanout
因为方法1和方法2监听的是同一个队列,只有一个可以消费成功。
多次执行,两个方法交替执行。
3.3 topic 主题交换机
主题交换机,会根据routing-Key的匹配规则,将消息发送到符合规则的队列中。
(一) 创建配置类
/**
* @className: TopicExchangeConfig
* @description:
* * (星号) 用来表示一个单词 (必须出现的)
* # (井号) 用来表示任意数量(零个或多个)单词
* @author: sh.Liu
* @date: 2021-08-23 15:49
*/
@Configuration
public class TopicExchangeConfig {
public static final String TOPIC_QUEUE = "topicQueue";
public static final String TOPIC_QUEUE2 = "topicQueue2";
public static final String TOPIC_QUEUE3 = "topicQueue3";
public static final String TOPIC_EXCHANGE = "topicExchange";
public static final String TOPIC_ROUTING_KEY = "topic*";
@Bean
public Queue topicQueue() {
return new Queue(TOPIC_QUEUE, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public Queue topicQueue3() {
return new Queue(TOPIC_QUEUE3, true);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE, true, false);
}
@Bean
public Binding bindingTopicExchange(Queue topicQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(topicQueue).to(topicExchange).with("topic.#");
}
@Bean
public Binding bindingTopicExchange2(Queue topicQueue2, TopicExchange topicExchange) {
return BindingBuilder.bind(topicQueue2).to(topicExchange).with("test.#");
}
@Bean
public Binding bindingTopicExchange3(Queue topicQueue3, TopicExchange topicExchange) {
return BindingBuilder.bind(topicQueue3).to(topicExchange).with("#");
}
}
这里要注意我们的绑定管关系。分别是topic.#, test.*, #
#: 代表所有,* 代表有且只有一个。
(二)消息生产者
消息的发送者,我们将routingKey作为参数方便我们看效果:
@RestController
@Slf4j
@RequestMapping("/topic")
public class TopicController {
private final RabbitTemplate rabbitTemplate;
public TopicController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@GetMapping("send")
public Object sendMsg(String routingKey) {
rabbitTemplate.convertAndSend(TopicExchangeConfig.TOPIC_EXCHANGE, routingKey, "发送一条测试消息:topic");
return "topic消息发送成功!!";
}
}
(三)消息消费者
消息的消费者:
/**
* @className: TopicQueueListener
* @description: 主题交换机的监听器
* @author: sh.Liu
* @date: 2021-08-23 16:03
*/
@Slf4j
@Component
public class TopicQueueListener {
/**
* topic: 主题交换机
* @param testMessage
*/
@RabbitHandler
@RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE)
public void process(String testMessage) {
System.out.println("TopicReceiver消费者收到消息1 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE)
public void process2(String testMessage) {
System.out.println("TopicReceiver消费者收到消息2 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE2)
public void process3(String testMessage) {
System.out.println("TopicReceiver消费者收到消息3 : " + testMessage);
}
@RabbitHandler
@RabbitListener(queues = TopicExchangeConfig.TOPIC_QUEUE3)
public void process4(String testMessage) {
System.out.println("TopicReceiver消费者收到消息4 : " + testMessage);
}
}
请求: http://localhost:7890/topic/send?routingKey=test.a
结果:
TopicReceiver消费者收到消息3 : 发送一条测试消息:topic
TopicReceiver消费者收到消息4 : 发送一条测试消息:topic
代表: test.* 和 # 与路由key匹配成功
请求: http://localhost:7890/topic/send?routingKey=topic.123
TopicReceiver消费者收到消息1 : 发送一条测试消息:topic
TopicReceiver消费者收到消息4 : 发送一条测试消息:topic
代表: topic.# 和 # 匹配成功
请求: http://localhost:7890/topic/send?routingKey=test
TopicReceiver消费者收到消息4 : 发送一条测试消息:topic
test.* 后面必须要有一个单词
请求: http://localhost:7890/topic/send?routingKey=test.aaa
TopicReceiver消费者收到消息4 : 发送一条测试消息:topic
TopicReceiver消费者收到消息3 : 发送一条测试消息:topic
test.*和 #匹配成功
请求: http://localhost:7890/topic/send?routingKey=test.aaa.b
TopicReceiver消费者收到消息4 : 发送一条测试消息:topic
只对# 匹配成功, 因为test.*只能匹配一个单词,aaa.b代表两个
(五)基于springBoot集成RabbitMQ的消息确认
参考文章
【1】SpringBoot整合RabbitMQ实现消息确认机制
【2】springboot + rabbitmq 消息确认机制
【3】Springboot 整合RabbitMq ,用心看完这一篇就够了
RabbitMQ的消息确认有两种。
第一种是消息发送确认。
这种是用来确认生产者将消息发送给交换器,交换器传递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。
第二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息。
5.1 对于 消息发送确认 的操作(消息生产者的配置)
(一)修改application.properties配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
####################################################
# 确认消息已发送到交换机(Exchange)
spring.rabbitmq.publisher-confirms=true
# 确认消息已发送到队列(Queue)
spring.rabbitmq.publisher-returns=true
(二)新建配置文件RabbitTemplate
对于 发送确认 写法有多种方式,以下的是其中的一种方式
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//指定消息在没有被队列接收时是否强行退回还是直接丢弃
//该项通常与yml配置文件中的publisher-returns配合一起使用,若不配置该项,setReutrnCallback将不会有消息返回
rabbitTemplate.setMandatory(true);
//消息发送成功的回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
});
//发生异常时的消息返回提醒
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("ReturnCallback: "+"消息:"+message);
System.out.println("ReturnCallback: "+"回应码:"+replyCode);
System.out.println("ReturnCallback: "+"回应信息:"+replyText);
System.out.println("ReturnCallback: "+"交换机:"+exchange);
System.out.println("ReturnCallback: "+"路由键:"+routingKey);
});
return rabbitTemplate;
}
}
5.2 对于 消费接收确认 的操作(消息消费者的配置-需要改为手动确认)
要想得知 消费是否接收成功 需要改成手动,然后再判断
(一)修改application.properties配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
####################################################
# 设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled=true
(二)修改Service接收信息项
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class DirectReceiver {
@RabbitHandler
@RabbitListener(queues = "emailQueue") //监听的队列名称
public void emailProcess(Channel channel, Message testMessage) throws IOException {
try{
System.out.println(new String(testMessage.getBody(),"UTF-8"));
//成功接收到消息后,确认消息,要不确认消息将会进入unacked,false只确认当前一个消息收到,true确认所有consumer获得的消息
channel.basicAck(testMessage.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//若是消息没有成功接收,第二个参数设置为true的话,代表重新放回队列中,false则为丢弃,在此也可以做成放置死信队列的操作
channel.basicReject(testMessage.getMessageProperties().getDeliveryTag(),false);
}
}
@RabbitHandler
@RabbitListener(queues = "smsQueue") //监听的队列名称
public void smsProcess(Channel channel, Message testMessage) throws IOException {
try{
System.out.println(new String(testMessage.getBody(),"UTF-8"));
channel.basicAck(testMessage.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//第二个参数,true代表会重新放回队列
channel.basicReject(testMessage.getMessageProperties().getDeliveryTag(),false);
}
}
}
方法解释:
- basicAck: 这个方法用于确认消息已被成功处理。第一个参数是消息的delivery tag(用于标识消息),第二个参数指定是否批量确认(
false
表示只确认当前消息)。- basicReject: 这个方法用于拒绝消息。第一个参数同样是delivery tag,第二个参数指定是否将消息重新放回队列(
false
表示不重新放回,即丢弃消息)。