本文主要对SpringBoot2.x集成RabbitMQ进行简单总结,其中SpringBoot使用的2.4.5
版本。
一、集成RabbitMQ
通过Maven新建一个名为springboot-rabbitmq-producer
的项目作为生产者发送消息,新建一个名为springboot-rabbitmq-consumer
的项目作为消费者接收消息。
1.引入依赖
除了SpringBoot的依赖外,生产者和消费者都还需引入amqp的依赖:
<!-- amqp起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.编写配置文件
在生产者和消费者的application.yml
中都进行如下配置:
spring:
rabbitmq:
# 配置RabbitMq的连接信息
host: 这里填RabbitMq服务器ip
port: 5672
username: admin
password: admin
virtual-host: /
3.创建配置类
在消费者中创建一个用于配置交换机和队列的配置类:
package com.rtxtitanv.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.config.RabbitMqConfig
* @description 配置交换机和队列
* @date 2021/4/17 16:48
*/
@Configuration
public class RabbitMqConfig {
}
4.创建测试类
在生产者中创建一个用于发送消息的测试类进行测试:
package com.rtxtitanv;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.RabbitMqTest
* @description 用于生产者发送消息的测试类
* @date 2021/4/17 17:02
*/
// SpringBoot2.4.0开始使用Junit5,不需要加@Runwith
@SpringBootTest(classes = RabbitMqProducerApplication.class)
class RabbitMqTest {
@Autowired
private RabbitTemplate rabbitTemplate;
}
5.创建接收消息的类
在消费者中创建一个接收消息的类:
package com.rtxtitanv.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.listener.RabbitMqListener
* @description 用于监听队列
* @date 2021/4/17 17:47
*/
@Component
public class RabbitMqListener {
private static Logger logger = LoggerFactory.getLogger(RabbitMqListener.class);
}
二、RabbitMQ常见的工作模式
1.简单模式
在配置类中进行如下配置:
/**
* 简单模式,声明队列
*
* @return Queue对象
*/
@Bean(name = "simpleQueue")
public Queue simpleQueue() {
/*
* 参数明细
* 1.name:队列名称
* 2.durable:是否持久化队列,true表示持久化,false表示不持久化
* 3.exclusive:是否独占此队列,true表示是,false表示否
* 4.autoDelete:队列不用是否自动删除,true表示是,false表示否
*/
return new Queue("simple-queue", true, false, false);
}
生产者发送消息:
/**
* 简单模式,发送消息测试
*/
@Test
void simpleSendTest() {
/*
* 发送消息
* 参数明细
* 1.exchange:交换机名称,如果没有指定,则使用Default Exchange
* 由于没指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
* 示绑定或解除绑定
* 2.routingKey:路由键,由于使用默认交换机,指定路由键为队列名
* 3.object:发送的消息
*/
rabbitTemplate.convertAndSend("", "simple-queue", "Hello World");
}
消费者接收消息:
@RabbitListener(queues = "simple-queue")
public void simpleQueue(String msg) {
logger.info("监听simple-queue队列的消费者接收到的消息:" + msg);
}
测试结果:
2.工作队列模式
在配置类中进行如下配置:
/**
* 工作队列模式,声明队列
*
* @return Queue对象
*/
@Bean(name = "workQueue")
public Queue workQueue() {
return new Queue("work-queue", true, false, false);
}
生产者发送消息:
/**
* 工作队列模式,发送消息测试
*/
@Test
void workSendTest() {
for (int i = 1; i <= 10; i++) {
rabbitTemplate.convertAndSend("", "work-queue", "message" + i);
}
}
消费者接收消息:
@RabbitListener(queues = "work-queue")
public void workQueue1(String msg) {
logger.info("监听work-queue队列的消费者1接收到的消息:" + msg);
}
@RabbitListener(queues = "work-queue")
public void workQueue2(String msg) {
logger.info("监听work-queue队列的消费者2接收到的消息:" + msg);
}
测试结果:
3.发布订阅模式
在配置类中进行如下配置:
/**
* 发布订阅模式,声明交换机
*
* @return FanoutExchange对象
*/
@Bean(name = "publishExchange")
public FanoutExchange publishExchange() {
// 创建一个fanout类型的交换机
return new FanoutExchange("publish-exchange");
}
/**
* 发布订阅模式,声明队列1
*
* @return Queue对象
*/
@Bean(name = "publishQueue1")
public Queue publishQueue1() {
return new Queue("publish-queue1", true, false, false);
}
/**
* 发布订阅模式,声明队列2
*
* @return Queue对象
*/
@Bean(name = "publishQueue2")
public Queue publishQueue2() {
return new Queue("publish-queue2", true, false, false);
}
/**
* 将队列publish-queue1与交换机publish-exchange绑定
*
* @param queue 队列
* @param fanoutExchange fanout类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue1ToFanoutExchange")
public Binding bindingQueue1ToFanoutExchange(@Qualifier("publishQueue1") Queue queue,
FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue).to(fanoutExchange);
}
/**
* 将队列publish-queue2与交换机publish-exchange绑定
*
* @param queue 队列
* @param fanoutExchange fanout类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue2ToFanoutExchange")
public Binding bindingQueue2ToFanoutExchange(@Qualifier("publishQueue2") Queue queue,
FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue).to(fanoutExchange);
}
生产者发送消息:
/**
* 发布订阅模式,发送消息测试
*/
@Test
void publishSendTest() {
rabbitTemplate.convertAndSend("publish-exchange", "", "Hello World");
}
消费者接收消息:
@RabbitListener(queues = "publish-queue1")
public void publishQueue1(String msg) {
logger.info("监听publish-queue1队列的消费者接收到的消息:" + msg);
}
@RabbitListener(queues = "publish-queue2")
public void publishQueue2(String msg) {
logger.info("监听publish-queue2队列的消费者接收到的消息:" + msg);
}
测试结果:
4.路由模式
在配置类中进行如下配置:
/**
* 路由模式,声明交换机
*
* @return DirectExchange对象
*/
@Bean(name = "routingExchange")
public DirectExchange routingExchange() {
// 创建一个direct类型的交换机
return new DirectExchange("routing-exchange");
}
/**
* 路由模式,声明队列1
*
* @return Queue对象
*/
@Bean(name = "routingQueue1")
public Queue routingQueue1() {
return new Queue("routing-queue1", true, false, false);
}
/**
* 路由模式,声明队列2
*
* @return Queue对象
*/
@Bean(name = "routingQueue2")
public Queue routingQueue2() {
return new Queue("routing-queue2", true, false, false);
}
/**
* 路由模式,声明队列3
*
* @return Queue对象
*/
@Bean(name = "routingQueue3")
public Queue routingQueue3() {
return new Queue("routing-queue3", true, false, false);
}
/**
* 将队列routing-queue1与交换机routing-exchange绑定,绑定键为info
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue1ToDirectExchangeWithInfo")
public Binding bindingQueue1ToDirectExchangeWithInfo(@Qualifier("routingQueue1") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("info");
}
/**
* 将队列routing-queue2与交换机routing-exchange绑定,绑定键为info
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue2ToDirectExchangeWithInfo")
public Binding bindingQueue2ToDirectExchangeWithInfo(@Qualifier("routingQueue2") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("info");
}
/**
* 将队列routing-queue1与交换机routing-exchange绑定,绑定键为warning
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean("bindingQueue1ToDirectExchangeWithWarning")
public Binding bindingQueue1ToDirectExchangeWithWarning(@Qualifier("routingQueue1") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("warning");
}
/**
* 将队列routing-queue3与交换机routing-exchange绑定,绑定键为warning
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean("bindingQueue3ToDirectExchangeWithWarning")
public Binding bindingQueue3ToDirectExchangeWithWarning(@Qualifier("routingQueue3") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("warning");
}
/**
* 将队列routing-queue2与交换机routing-exchange绑定,绑定键为error
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue2ToDirectExchangeWithError")
public Binding bindingQueue2ToDirectExchangeWithError(@Qualifier("routingQueue2") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("error");
}
/**
* 将队列routing-queue3与交换机routing-exchange绑定,绑定键为error
*
* @param queue 队列
* @param directExchange direct类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue3ToDirectExchangeWithError")
public Binding bindingQueue3ToDirectExchangeWithError(@Qualifier("routingQueue3") Queue queue,
DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("error");
}
生产者发送消息:
/**
* 路由模式,发送消息测试
*/
@Test
void routingSendTest() {
rabbitTemplate.convertAndSend("routing-exchange", "info", "日志信息");
rabbitTemplate.convertAndSend("routing-exchange", "warning", "警告信息");
rabbitTemplate.convertAndSend("routing-exchange", "error", "错误信息");
}
消费者接收消息:
@RabbitListener(queues = "routing-queue1")
public void routingQueue1(String msg) {
logger.info("监听routing-queue1队列的消费者接收到的消息:" + msg);
}
@RabbitListener(queues = "routing-queue2")
public void routingQueue2(String msg) {
logger.info("监听routing-queue2队列的消费者接收到的消息:" + msg);
}
@RabbitListener(queues = "routing-queue3")
public void routingQueue3(String msg) {
logger.info("监听routing-queue3队列的消费者接收到的消息:" + msg);
}
也可以不在配置类中声明交换机队列及绑定,可以在@RabbitListener
中配置队列和交换机以及它们之间的绑定关系:
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "routing-queue1", durable = "true", exclusive = "false", autoDelete = "false"),
exchange = @Exchange(value = "routing-exchange", type = "direct"), key = {"info", "warning"})})
public void routingQueue1(String msg) {
logger.info("监听routing-queue1队列的消费者接收到的消息:" + msg);
}
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "routing-queue2", durable = "true", exclusive = "false", autoDelete = "false"),
exchange = @Exchange(value = "routing-exchange", type = "direct"), key = {"info", "error"})})
public void routingQueue2(String msg) {
logger.info("监听routing-queue2队列的消费者接收到的消息:" + msg);
}
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "routing-queue3", durable = "true", exclusive = "false", autoDelete = "false"),
exchange = @Exchange(value = "routing-exchange", type = "direct"), key = {"warning", "error"})})
public void routingQueue3(String msg) {
logger.info("监听routing-queue3队列的消费者接收到的消息:" + msg);
}
测试结果:
5.主题模式
在配置类中进行如下配置:
/**
* 主题模式,声明交换机
*
* @return TopicExchange对象
*/
@Bean(name = "topicsExchange")
public TopicExchange topicsExchange() {
// 创建一个topic类型的交换机
return new TopicExchange("topics-exchange");
}
/**
* 主题模式,声明队列1
*
* @return Queue对象
*/
@Bean(name = "topicsQueue1")
public Queue topicsQueue1() {
return new Queue("topics-queue1", true, false, false);
}
/**
* 主题模式,声明队列2
*
* @return Queue对象
*/
@Bean(name = "topicsQueue2")
public Queue topicsQueue2() {
return new Queue("topics-queue2", true, false, false);
}
/**
* 主题模式,声明队列3
*
* @return Queue对象
*/
@Bean(name = "topicsQueue3")
public Queue topicsQueue3() {
return new Queue("topics-queue3", true, false, false);
}
/**
* 将队列topics-queue1与交换机topics-exchange绑定,绑定键为*.rabbitmq.*
*
* @param queue 队列
* @param topicExchange topic类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue1ToTopicExchange")
public Binding bindingQueue1ToTopicExchange(@Qualifier("topicsQueue1") Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("*.rabbitmq.*");
}
/**
* 将队列topics-queue2与交换机topics-exchange绑定,绑定键为*.*.server
*
* @param queue 队列
* @param topicExchange topic类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue2ToTopicExchange")
public Binding bindingQueue2ToTopicExchange(@Qualifier("topicsQueue2") Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("*.*.server");
}
/**
* 将队列topics-queue3与交换机topics-exchange绑定,绑定键为com.#
*
* @param queue 队列
* @param topicExchange topic类型交换机
* @return Binding对象
*/
@Bean(name = "bindingQueue3ToTopicExchange")
public Binding bindingQueue3ToTopicExchange(@Qualifier("topicsQueue3") Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("com.#");
}
生产者发送消息:
/**
* 主题模式,发送消息测试
*/
@Test
void topicsSendTest() {
rabbitTemplate.convertAndSend("topics-exchange", "www.rabbitmq.server", "message1");
rabbitTemplate.convertAndSend("topics-exchange", "com.rabbitmq.client", "message2");
rabbitTemplate.convertAndSend("topics-exchange", "com.kafka.server", "message3");
}
消费者接收消息:
@RabbitListener(queues = "topics-queue1")
public void topicsQueue1(String msg) {
logger.info("监听topics-queue1队列的消费者接收到的消息:" + msg);
}
@RabbitListener(queues = "topics-queue2")
public void topicsQueue2(String msg) {
logger.info("监听topics-queue2队列的消费者接收到的消息:" + msg);
}
@RabbitListener(queues = "topics-queue3")
public void topicsQueue3(String msg) {
logger.info("监听topics-queue3队列的消费者接收到的消息:" + msg);
}
测试结果:
三、消息确认
RabbitMQ的消息确认有两种:
- 发送方确认:确认生产者是否将消息发送给交换机,交换机是否成功将消息传递给队列。
- 接收方确认:确认消费者是否成功消费了队列中的消息。
1.发送方确认
发送方确认有两步,对应两种模式:
- confirm模式:confirm确认消息是否从生产者到达交换机。消息从生产者到交换机则会调用回调函数confirmCallback。
- return模式:return确认消息是否从交换机到达队列。消息从交换机到达队列失败会调用回调函数returnCallback。
在生产者的配置文件中新增以下配置:
spring:
rabbitmq:
# 配置发送方确认机制
# 设置confirm类型为correlated,确认消息是否到达交换机
publisher-confirm-type: correlated
# 启用return模式,确认消息是否到达队列
publisher-returns: true
生产者发送消息:
/**
* 测试发送方确认机制
*
* @throws InterruptedException
*/
@Test
void producerConfirm() throws InterruptedException {
// 开启Mandatory,才能触发回调函数,如果设置为false,则关闭Mandatory,消息直接丢弃
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
logger.info("ConfirmCallback---相关数据:" + correlationData);
logger.info("ConfirmCallback---确认情况:" + ack);
logger.info("ConfirmCallback---原因:" + cause);
});
rabbitTemplate.setReturnsCallback(returnedMessage -> {
// 消息对象
Message message = returnedMessage.getMessage();
// 回应码
int replyCode = returnedMessage.getReplyCode();
// 回应信息
String replyText = returnedMessage.getReplyText();
// 交换机
String exchange = returnedMessage.getExchange();
// 路由键
String routingKey = returnedMessage.getRoutingKey();
logger.info("ReturnsCallback---消息对象:" + message);
logger.info("ReturnsCallback---回应码:" + replyCode);
logger.info("ReturnsCallback---回应信息:" + replyText);
logger.info("ReturnsCallback---交换机:" + exchange);
logger.info("ReturnsCallback---路由键:" + routingKey);
});
// rabbitTemplate.convertAndSend("confirm-exchange1", "info", "message");
// rabbitTemplate.convertAndSend("confirm-test-exchange", "info", "message");
rabbitTemplate.convertAndSend("confirm-exchange", "info", "message");
// 为了便于测试,发送消息之后让线程休眠一会,避免测试方法结束之后因资源关闭导致confirmCallback出错
Thread.sleep(2000);
}
消费者接收消息:
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "confirm-queue", durable = "true", exclusive = "false", autoDelete = "false"),
exchange = @Exchange(value = "confirm-exchange", type = ExchangeTypes.DIRECT), key = {"info"})})
public void producerConfirmQueue(String msg) {
logger.info("监听confirm-queue队列的消费者接收到的消息:" + msg);
}
在消息没有成功到达交换机的情况下。这里在发送消息时指定了一个不存在的交换机,测试结果如下:
这种情况触发了ConfirmCallback
回调函数。由于消息没有到达指定交换机,返回的确认情况(ack)为false。
在消息成功到达了交换机但交换机没有成功将消息传递到队列的情况下。这里新建一个交换机并且不绑定任何队列,在配置类中进行如下配置:
@Bean(name = "confirmTestExchange")
public DirectExchange confirmTestExchange() {
// 创建一个direct类型的交换机
return new DirectExchange("confirm-test-exchange");
}
然后在发送消息时指定该交换机,测试结果如下:
这种情况触发了ConfirmCallback
和ReturnsCallback
回调函数。由于消息到达了指定交换机,返回的确认情况(ack)为true。但是在路由到队列时没找到队列,返回了回应信息NO_ROUTE。
在消息成功到达了交换机并且交换机成功将消息传递到队列的情况下。测试结果如下:
这种情况触发了ConfirmCallback
回调函数。
2.接收方确认
RabbitMQ提供了一种ack模式(acknowledge mode)用于接收方确认。ack模式有3种确认方式:
- NONE:自动确认
- MANUAL:手动确认
- AUTO:根据异常情况确认
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应message从RabbitMQ的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
在消费者的配置文件中新增以下配置:
spring:
rabbitmq:
listener:
simple:
# 配置接收方确认机制
# NONE:自动确认,MANUAL:手动确认,AUTO:根据异常情况确认
# acknowledgeMode设置为手动模式
acknowledge-mode: manual
direct:
acknowledge-mode: manual
生产者发送消息:
/**
* 测试接收方确认机制
*
* @throws InterruptedException
*/
@Test
void consumerAck() throws InterruptedException {
// 开启Mandatory,才能触发回调函数,如果设置为false,则关闭Mandatory,消息直接丢弃
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
logger.info("消息发送到交换机成功");
} else {
logger.error("消息发送到交换机失败");
logger.error("失败原因:" + cause);
}
});
rabbitTemplate.setReturnsCallback(returnedMessage -> {
logger.error("消息从交换机到队列失败");
});
// rabbitTemplate.convertAndSend("ack-exchange", "info", 2);
rabbitTemplate.convertAndSend("ack-exchange", "info", 0);
// 为了便于测试,发送消息之后让线程休眠一会,避免测试方法结束之后因资源关闭导致confirmCallback出错
Thread.sleep(2000);
}
消费者接收消息:
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(value = "ack-queue", durable = "true", exclusive = "false", autoDelete = "false"),
exchange = @Exchange(value = "ack-exchange", type = ExchangeTypes.DIRECT), key = {"info"})})
public void consumerAckQueue(Integer msg, Channel channel, Message message) throws IOException {
try {
logger.info("监听ack-queue队列的消费者接收到的消息:" + msg);
logger.info("消息处理开始");
int i = 10 / msg;
logger.info("消息处理成功");
logger.info("10除msg等于" + i);
// 肯定确认,确认消息已经成功消费,可以从队列删除
// 参数1:当前消息的唯一id
// 参数2:是否批量确认,true表示一次确认所有小于deliveryTag的消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.error("消息处理失败");
// 否定确认,确认消息没有成功消费
// 参数1:当前消息的唯一id
// 参数2:是否批量确认,true表示一次确认所有小于deliveryTag的消息
// 参数3:消息是否重新入队,true表示将确认为没有成功消费的消息重新放入队列,是否重新入队可以根据具体业务逻辑进行设置
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
消息处理成功时的测试结果:
设置确认为没有成功消费的消息不重新入队,在消息处理失败时的测试结果:
设置确认为没有成功消费的消息重新入队,即:
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
设置确认为没有成功消费的消息重新入队,消息处理失败时的测试结果:
由于这里只是简单测试,消息处理失败后重新放入队列后还是会处理失败(上图只截了一部分),所以确认为没有成功消费的消息是否重新入队可以根据具体业务逻辑进行设置。设置确认为没有成功消费的消息重新入队,可以在消费者处理消息失败时,重新从队列接收该消息。发送方确认机制、接收方确认机制和RabbitMQ持久化机制(交换机、队列、发送的消息持久化)保证了RabbitMQ消息的可靠性传输,避免消息丢失。
除了在配置文件中配置接收方确认机制外,还可以自定义SimpleMessageListenerContainer
监听容器。在消费者中新建一个RabbitMqListenerConfig
配置类:
package com.rtxtitanv.config;
import com.rtxtitanv.listener.AckListener;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.config.RabbitMqListenerConfig
* @description 配置自定义监听容器和自定义监听容器工厂
* @date 2021/4/21 15:41
*/
@Configuration
public class RabbitMqListenerConfig {
@Resource
private CachingConnectionFactory cachingConnectionFactory;
@Resource
private AckListener ackListener;
/**
* 自定义监听容器
*
* @return SimpleMessageListenerContainer
*/
@Bean(name = "simpleMessageListenerContainer1")
public SimpleMessageListenerContainer simpleMessageListenerContainer1() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory);
// 设置接收方确认模式,AcknowledgeMode.MANUAL为手动确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 设置监听队列,多个队列可以用,号隔开,队列必须已经创建
container.setQueueNames("ack-test-queue1", "ack-test-queue2");
// 设置消息监听类
container.setMessageListener(ackListener);
return container;
}
}
在配置类中配置两个用于测试的队列:
@Bean(name = "ackTestQueue1")
public Queue ackTestQueue1() {
return new Queue("ack-test-queue1", true, false, false);
}
@Bean(name = "ackTestQueue2")
public Queue ackTestQueue2() {
return new Queue("ack-test-queue2", true, false, false);
}
创建用于手动确认模式的消息监听类,需实现ChannelAwareMessageListener
接口:
package com.rtxtitanv.listener;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.listener.AckListener
* @description 用于手动确认模式的消息监听类,需实现ChannelAwareMessageListener接口
* @date 2021/4/21 16:23
*/
@Component
public class AckListener implements ChannelAwareMessageListener {
private static Logger logger = LoggerFactory.getLogger(AckListener.class);
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String consumerQueue = message.getMessageProperties().getConsumerQueue();
try {
String msg = new String(message.getBody());
int len = msg.length();
if ("ack-test-queue1".equals(consumerQueue)) {
logger.info("消息来自的队列:" + consumerQueue);
logger.info("消息:" + msg);
logger.info("消息处理开始");
int i = 10 / len;
logger.info("消息处理成功");
logger.info("10除消息长度等于" + i);
}
if ("ack-test-queue2".equals(consumerQueue)) {
logger.info("消息来自的队列:" + consumerQueue);
logger.info("消息:" + msg);
logger.info("消息处理开始");
int i = 10 / len;
logger.info("消息处理成功");
logger.info("10除消息长度等于" + i);
}
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
logger.error("消息处理失败");
channel.basicNack(deliveryTag, false, false);
}
}
}
生产者发送消息:
/**
* 测试接收方确认机制
*/
@Test
void consumerAck2() {
rabbitTemplate.convertAndSend("", "ack-test-queue1", "Hello");
rabbitTemplate.convertAndSend("", "ack-test-queue2", "");
}
在测试之前将配置文件中的接收方确认配置注释掉,测试结果:
四、并发消费与限流
1.并发
默认情况一下,一个listener对应一个consumer,如果想要对应多个,需指定并发消费者数量。第一种方式,在消费者的配置文件中新增以下配置:
spring:
rabbitmq:
listener:
simple:
# 配置并发消费
# 并发消费者最小数量
concurrency: 5
# 并发消费者最大数量
max-concurrency: 10
注意:这个是全局配置,该配置对应用中的任何listener都会生效,如果只需要少数listener对应多个consumer,则不用此配置。
生产者发送消息:
/**
* 测试并发消费
*/
@Test
void concurrencyConsumer() {
for (int i = 1; i <= 10; i++) {
rabbitTemplate.convertAndSend("", "concurrency-test-queue", "message" + i);
}
}
消费者接收消息:
/**
* 监听concurrency-test-queue队列
* 通过@RabbitListener的queuesToDeclare属性声明一个队列,绑定默认交换机,路由键为队列名
*
* @param msg 消息
*/
@RabbitListener(queuesToDeclare = @Queue("concurrency-test-queue"))
public void concurrencyConsumerQueue(String msg) {
logger.info(Thread.currentThread().getName() + "接收的消息:" + msg);
}
启动消费者后,在RabbitMQ管理页面可以看到该队列对应5个consumer:
测试结果:
第二种方式,可以使用@RabbitListener
的concurrency
属性设置并发消费者数量:
/**
* 监听concurrency-test-queue队列
* 通过@RabbitListener的queuesToDeclare属性声明一个队列,绑定默认交换机,路由键为队列名
* concurrency min-max设置并发消费者数,min为并发消费者最小数量,max为并发消费者最大数量
*
* @param msg 消息
*/
@RabbitListener(queuesToDeclare = @Queue("concurrency-test-queue"), concurrency = "5-10")
public void concurrencyConsumerQueue(String msg) {
logger.info(Thread.currentThread().getName() + "接收的消息:" + msg);
}
在测试之前将配置文件中的并发消费配置注释掉,启动消费者后,在RabbitMQ管理页面可以看到该队列对应5个consumer:
测试结果:
第三种方式,可以自定义SimpleMessageListenerContainer
监听容器:
@Resource
private ConcurrencyListener concurrencyListener;
/**
* 自定义监听容器
*
* @return SimpleMessageListenerContainer
*/
@Bean(name = "simpleMessageListenerContainer2")
public SimpleMessageListenerContainer simpleMessageListenerContainer2() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory);
// 设置并发消费者最小数量
container.setConcurrentConsumers(5);
// 设置并发消费者最大数量
container.setMaxConcurrentConsumers(10);
// 设置接收方确认模式,AcknowledgeMode.MANUAL为手动确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 设置监听队列,多个队列可以用,号隔开,队列必须已经创建
container.setQueueNames("concurrency-test-queue");
// 设置消息监听类
container.setMessageListener(concurrencyListener);
return container;
}
在配置类中配置用于测试的队列concurrency-test-queue
:
@Bean(name = "concurrencyTestQueue")
public Queue concurrencyTestQueue() {
return new Queue("concurrency-test-queue", true, false, false);
}
自定义的用于手动确认模式的消息监听器类ConcurrencyListener
:
package com.rtxtitanv.listener;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.listener.ConcurrencyListener
* @description 用于手动确认模式的消息监听类,需实现ChannelAwareMessageListener接口
* @date 2021/4/22 17:58
*/
@Component
public class ConcurrencyListener implements ChannelAwareMessageListener {
private static Logger logger = LoggerFactory.getLogger(ConcurrencyListener.class);
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String consumerQueue = message.getMessageProperties().getConsumerQueue();
try {
String msg = new String(message.getBody());
if ("concurrency-test-queue".equals(consumerQueue)) {
logger.info(Thread.currentThread().getName() + "接收的消息:" + msg);
}
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
logger.error("消息处理失败");
channel.basicNack(deliveryTag, false, false);
}
}
}
在测试前将第二种方式中用@RabbitListener
注解的监听器注释掉并将之前的队列concurrency-test-queue
删除,测试结果:
2.限流
第一种方式,在消费者的配置文件中新增以下配置:
spring:
rabbitmq:
listener:
simple:
# 消费端限流配置,每个consumer在单位时间内接收的消息数,默认为250
prefetch: 50
此配置为全局配置,启动消费者后,查看队列:
第二种方式,可以使用@RabbitListener
的containerFactory
属性指定一个自定义监听容器工厂:
/**
* 监听prefetch-test-queue队列
* containerFactory 指定自定义监听容器工厂
*
* @param msg 消息
*/
@RabbitListener(queuesToDeclare = @Queue("prefetch-test-queue"), concurrency = "5-10",
containerFactory = "simpleRabbitListenerContainerFactory")
public void concurrencyConsumerQueue(String msg) {
logger.info(Thread.currentThread().getName() + "接收的消息:" + msg);
}
在自定义监听容器工厂中设置PrefetchCount:
/**
* 自定义监听容器工厂
*
* @return SimpleRabbitListenerContainerFactory
*/
@Bean(name = "simpleRabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(cachingConnectionFactory);
// 设置每个consumer在单位时间内接收的消息数
factory.setPrefetchCount(10);
return factory;
}
启动消费者后,查看prefetch-test-queue
队列,可以发现Prefetch count已设置为10:
第三种方式,可以在自定义SimpleMessageListenerContainer
监听容器中设置PrefetchCount:
@Resource
private PrefetchListener prefetchListener;
/**
* 自定义监听容器
*
* @return SimpleMessageListenerContainer
*/
@Bean(name = "simpleMessageListenerContainer3")
public SimpleMessageListenerContainer simpleMessageListenerContainer3() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory);
// 设置并发消费者最小数量
container.setConcurrentConsumers(5);
// 设置并发消费者最大数量
container.setMaxConcurrentConsumers(10);
// 设置每个consumer在单位时间内接收的消息数
container.setPrefetchCount(20);
// 设置接收方确认模式,AcknowledgeMode.MANUAL为手动确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 设置监听队列,多个队列可以用,号隔开,队列必须已经创建
container.setQueueNames("prefetch-test-queue");
// 设置消息监听类
container.setMessageListener(prefetchListener);
return container;
}
在配置类中配置用于测试的队列prefetch-test-queue
:
@Bean(name = "prefetchTestQueue")
public Queue prefetchTestQueue() {
return new Queue("prefetch-test-queue", true, false, false);
}
自定义的用于手动确认模式的消息监听器类PrefetchListener
:
package com.rtxtitanv.listener;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.listener.PrefetchListener
* @description 用于手动确认模式的消息监听类,需实现ChannelAwareMessageListener接口
* @date 2021/4/22 19:24
*/
@Component
public class PrefetchListener implements ChannelAwareMessageListener {
private static Logger logger = LoggerFactory.getLogger(PrefetchListener.class);
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String consumerQueue = message.getMessageProperties().getConsumerQueue();
try {
String msg = new String(message.getBody());
if ("prefetch-test-queue".equals(consumerQueue)) {
logger.info(Thread.currentThread().getName() + "接收的消息:" + msg);
}
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
logger.error("消息处理失败");
channel.basicNack(deliveryTag, false, false);
}
}
}
将第二种方式中用@RabbitListener
注解的监听器注释掉并将之前的队列concurrency-test-queue
删除后,重新启动消费者,查看prefetch-test-queue
队列,可以发现Prefetch count已设置为20:
代码示例
- Github:
https://github.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-rabbitmq-producer
https://github.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-rabbitmq-consumer - Gitee:
https://gitee.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-rabbitmq-producer
https://gitee.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-rabbitmq-consumer