目录
前言
RabbitMQ全栈实践手册:从零搭建消息中间件到SpringAMQP高阶玩法-CSDN博客
经过上次RabbitMQ的学习,基本具备了MQ收发消息的功能,可以满足大多数的业务场景,但是在
一些业务场景中我们要保证消息的可靠性,所以我们还需要学习一些RabbitMQ的一些高级功能,
例如:生产者确认、消费者重试、死信队列、延迟消息等。
消息可靠性
支付成功后,需要发送消息到broker,broker再发送消息给交易服务,但是这个过程消息可能丢
失,可能网络问题,有可以MQ挂了,这时候消息者那边已经支付了,但是订单状态还是未支付,
因为交易服务没有接收到消息,消息丢了。
消息丢失主要有三个地方:
- 发送者发送消息到broker这个过程可能消息丢失。
- 消息到达broker,broker可能挂了有可能消息丢失。
- broker发送消息到消费者或者已经到达消费者,没有正确处理也会消息丢失。
发送者可靠性
发送者重连
有时候可能网络问题导致发送消息到broker失败的情况,这个时候我们可以通过配置开启失败后的
重连。
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
现在我将mq停止掉了,配置了mq重连配置,再去发送mq可以看到重试了三次。
当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
发送者确认
Spring AMQP提供了Publisher Confirm和Publisher Returen机制,Publisher Confirm是确认消息是
否成功到达Exchange,Publisher Return处理从Exchange路由到Queue失败场景。
- 消息投递到MQ,但是路由失败,此时通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功。
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功。
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功。
- 其他情况都会返回NACK,告知投递失败。
配置文件中添加配置开启Publisher Confirm机制和Publisher Return机制
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
publisher-confirm-type配置说明:
none:关闭confirm机制
simple:同步阻塞等待MQ的回执消息
correlated:MQ异步回调方式返回回执消息
配置ReturnCallbacke,每个RabbitTemplate只能配置一个ReturnCallback
@Slf4j
@RequiredArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("触发return callback,");
log.debug("exchange:{}",returnedMessage.getExchange());
log.debug("routingKey:{}",returnedMessage.getMessage());
log.debug("message:{}",returnedMessage.getMessage());
log.debug("replyCode:{}",returnedMessage.getReplyCode());
log.debug("replyText:{}",returnedMessage.getReplyText());
}
});
}
}
发送消息,指定消息ID、消息ConfirmCallback
@Test
public void testConfirmCallback() throws InterruptedException {
// 创建correlationData
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
log.error("spring amqp处理确认结果异常",ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 判断是否成功
if(result.isAck()){
log.debug("判断ConfirmCallback ack 消息发送成功!");
}else{
log.error("收到ConfirmCallback nack 消息发送失败!reason:{}",result.getReason());
}
}
});
String exchangeName = "hmall.direct2";
String message = "陶然同学学习RabbitMQ高级篇";
rabbitTemplate.convertAndSend(exchangeName,"blue",message,cd);
Thread.sleep(2000);
}
效果:
MQ可靠性
默认情况下,RabbitMQ会把接收到的消息放到内存,如果内存慢了再把消息放到磁盘。这样就会
导致两个问题:
- MQ如果宕机,消息在内存里面就会丢失。
- 内存空间有限,消费者故障或者处理慢,就会消息积压,造成MQ阻塞。
数据持久化
Rabbit实现数据持久化主要有三个方面:
- 交换机持久化
- 队列持久化
- 消息持久化
我们使用Spring AMQP发送的交换机、队列、消息都是持久化的,即使MQ宕机重启,消息也还
在。
Lazy Queue
RabbitMQ从3.6.0版本开始,增加Lazy Queue概念,叫惰性队列,接收到消息直接存储在磁盘,不
再存储到内存,消费者消费消息的时候直接从磁盘加载到内存(有可以提前缓存部分消息到内存,
最多2048条)
消费者可靠性
消费者确认机制
消费者确认机制是为了消费者是否正确处理消息,当消费者处理消息结束后,应该向RabbitMQ发
送一个回执,告诉RabbitMQ自己处理消息状态:
- ack:成功处理消息,RabbitMQ从队列删除该消息。
- nack:消息处理失败,RabbitMQ需要再次投递消息
- reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
Spring AMQP已经实现了消息确认机制,我们在配置文件配置即可,有三种方式:
none:不处理,接收到消息返回ack给broker,broker删除消息。
manual:收到模式,自己在业务代理里返回ack或者reject。
auto:自动模式,利用AOP对业务代码做环绕通知,正常处理完返回ack,出现异常返回nack
业务异常会自动返回nack
消息处理或校验异常返回reject
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: none # none,关闭ack;manual,手动ack;auto:自动ack
失败重试机制
消费者返回nack,mq就会重新投递消息,然后消费者又返回nack,mq有重新投递,无限循环,我
们希望消费者处理错误消息的时候,能够通知我们开发,由我们开发介入。
SpringAMQP提供了消费者失败重试机制,通过配置文件开启。
spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1秒
multiplier: 1 # 下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
失败重试策略:
本地重试到达最大次数后,消息会被丢弃,对于某些可靠性要求较高的场景下,显然不太合适,因
此Spring允许自定义重试次数耗尽的消息处理策略,这个策略有MessageRecovery接口定义,它
有3个不同实现:
RejectAndDontRequestRecoverer:重试耗尽,直接reject,丢弃消息,默认实现。
ImmediateRequeueMessageRecoverer:重试耗尽,返回nack,消息重新入队。
RepublishMessageRecoverer:重试耗尽,将消息投递给指定交换机。
失败后将消息投递给一个指定的,专门存放异常消息队列,后续由人工集中处理。
@Configuration
public class ErrorMessageConfiguration {
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
@Bean
public Binding errorQueueBinding(Queue errorQueue,DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
业务幂等性
幂等性是在程序开发中,指同一个业务执行一次或多次对业务状态的影响是一致的。
唯一消息ID
方案一,是给每个消息都设置一个唯一id,利用id区分消息是否重复消费:
- 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
- 消费者接收到消息后处理自己的业务,业务处理完成后将消息ID保存到数据库。
- 如果下次有收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
业务判断
方案二,是结合业务逻辑,基于业务本身做判断。以我们的余额支付业务为例:
方案二,是结合业务逻辑,基于业务本身做判断。以我们的余额支付业务为例:
延迟消息
延迟消息:发送者发送消息时指定一个时间,消费者不会立即收到消息,而是在指定时间之后才收
到消息。
延迟任务:设置一定时间之后才执行的任务。
死信交换机
当一个队列中的消息满足下列情况之一时,就会称为死信(dead letter):
- 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息(到达了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个
交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)
代码实现
声明normal.direct交换机、normal.queue队列并且指定死信交换机。
@Configuration
public class NormalConfiguration {
@Bean
public DirectExchange normalExchange(){
return new DirectExchange("normal.direct");
}
@Bean
public Queue normalQueue(){
return QueueBuilder
.durable("normal.queue")
.deadLetterExchange("dlx.direct")
.build();
}
@Bean
public Binding normalExchangeBinding(Queue normalQueue,DirectExchange normalExchange){
return BindingBuilder.bind(normalQueue).to(normalExchange).with("hi");
}
}
监听dlx.direct交换机,死信交换机的routingkey要和normal.direct的toutingkey一样。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dlx.queue",durable = "true"),
exchange = @Exchange(name = "dlx.direct",type = ExchangeTypes.DIRECT),
key = "hi"
))
public void listenDlxQueue(String message){
log.info("消费者监听到dlx.queue的消息:【{}】",message);
}
发送消息,并指定消息过期时间为10秒钟。
@Test
void testSendDelayMessage(){
rabbitTemplate.convertAndSend("normal.direct","hi","hello 我叫陶然",message -> {
message.getMessageProperties().setExpiration("10000");
return message;
});
}
消息投递到normal.direct交换机,normal.direct交换机投递给normal.queue队列,normal.queue10
秒后没有人处理消息,将消息投递给dlx.direct交换机,dlx.direct交换机投递给dlx.queue队列。
延迟消息插件
基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插
件来是实现相同效果。
下载
官方文档说明:
Scheduling Messages with RabbitMQ | RabbitMQ
安装
因为我们是基于Docker安装,所以需要先查看RabbitMQ的插件目录对应的数据卷。
docker volume inspect mq-plugins
结果如下:
[
{
"CreatedAt": "2024-06-19T09:22:59+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mq-plugins/_data",
"Name": "mq-plugins",
"Options": null,
"Scope": "local"
}
]
插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data
这个目录,我们上传插件到该目
录下。
接下来执行命令,安装插件:
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
运行结果如下:
声明延迟交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue",durable = "true"),
exchange = @Exchange(name = "delay.direct",delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}",msg);
}
发送延迟消息
@Test
void testPublisherDelayMessage(){
//1、创建消息
String message = "hello,delayed message";
//2、发送消息 利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
效果
抽取MQ工具
共享配置
首先,我们需要在nacos中抽取RabbitMQ的共享配置,命名为shared-mq.yaml
:
其中只包含mq的基础共享配置,内容如下:
spring:
rabbitmq:
host: ${hm.mq.host:192.168.150.101} # 主机名
port: ${hm.mq.port:5672} # 端口
virtual-host: ${hm.mq.vhost:/hmall} # 虚拟主机
username: ${hm.mq.un:hmall} # 用户名
password: ${hm.mq.pw:123} # 密码
封装工具
package com.hmall.common.utils;
import cn.hutool.core.lang.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.util.concurrent.ListenableFutureCallback;
/**
* RabbitMQ消息发送工具类
* 提供普通消息、延迟消息、带确认机制消息的发送能力
*/
@Slf4j
@RequiredArgsConstructor
public class RabbitMqHelper {
// 通过构造器注入RabbitTemplate(由Spring自动装配)
private final RabbitTemplate rabbitTemplate;
/**
* 发送普通消息(不带确认机制)
* @param exchange 交换机名称
* @param routingKey 路由键
* @param msg 消息内容(会被自动序列化)
*/
public void sendMessage(String exchange, String routingKey, Object msg) {
log.debug("准备发送消息,exchange:{}, routingKey:{}, msg:{}", exchange, routingKey, msg);
// 使用convertAndSend实现自动消息转换
rabbitTemplate.convertAndSend(exchange, routingKey, msg);
}
/**
* 发送延迟消息(需要安装rabbitmq_delayed_message_exchange插件)
* @param exchange 交换机名称(必须是x-delayed-message类型)
* @param routingKey 路由键
* @param msg 消息内容
* @param delay 延迟时间(单位:毫秒)
*/
public void sendDelayMessage(String exchange, String routingKey, Object msg, int delay) {
// 通过MessagePostProcessor设置延迟参数
rabbitTemplate.convertAndSend(exchange, routingKey, msg, message -> {
// 设置消息的延迟时间(插件识别的header)
message.getMessageProperties().setDelay(delay);
return message;
});
}
/**
* 带确认机制的消息发送(支持失败重试)
* @param exchange 交换机名称
* @param routingKey 路由键
* @param msg 消息内容
* @param maxRetries 最大重试次数(建议3-5次)
*
* 实现特性:
* 1. 使用CorrelationData跟踪消息状态
* 2. 通过ListenableFuture实现异步确认
* 3. 失败后自动重试直至最大次数
*
* 注意:频繁重试可能影响性能,建议结合死信队列使用
*/
public void sendMessageWithConfirm(String exchange, String routingKey, Object msg, int maxRetries) {
log.debug("准备发送消息(带确认),exchange:{}, routingKey:{}, msg:{}", exchange, routingKey, msg);
// 生成唯一消息ID用于追踪
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString(true));
// 添加确认结果回调
cd.getFuture().addCallback(new ListenableFutureCallback<>() {
// 当前重试次数计数器
int retryCount = 0;
/**
* 处理确认失败(网络异常等)
*/
@Override
public void onFailure(Throwable ex) {
log.error("处理ACK回执时发生异常", ex);
}
/**
* 处理Broker确认结果
* @param result 确认结果对象(包含ack/nack状态)
*/
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 检查是否为NACK(消息未到达Broker)
if (result != null && !result.isAck()) {
log.warn("收到Broker的NACK确认,消息未到达,当前重试次数:{}", retryCount);
// 判断是否超过最大重试次数
if (retryCount >= maxRetries) {
log.error("消息重试次数耗尽(最大{}次),放弃发送", maxRetries);
return;
}
// 生成新的消息ID(避免重复消息问题)
CorrelationData newCd = new CorrelationData(UUID.randomUUID().toString(true));
// 保持回调链
newCd.getFuture().addCallback(this);
// 重新发送消息
rabbitTemplate.convertAndSend(exchange, routingKey, msg, newCd);
retryCount++;
log.debug("正在进行第{}次重试...", retryCount);
} else {
log.debug("消息已成功到达Broker");
}
}
});
// 执行首次消息发送
rabbitTemplate.convertAndSend(exchange, routingKey, msg, cd);
}
}
自动装配
该配置类主要用于 整合RabbitMQ的消息序列化及工具类初始化,通过条件化加载机制确保在满足
依赖时自动配置生效。
package com.hmall.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hmall.common.utils.RabbitMqHelper;
import org.springframework.amqp.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(value = {MessageConverter.class, RabbitTemplate.class})
public class MqConfig {
@Bean
@ConditionalOnBean(ObjectMapper.class)
public MessageConverter messageConverter(ObjectMapper mapper){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(mapper);
// 2.配置自动创建消息id,用于识别不同消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
@Bean
public RabbitMqHelper rabbitMqHelper(RabbitTemplate rabbitTemplate){
return new RabbitMqHelper(rabbitTemplate);
}
}