一. 发送者的可靠性(非重点)
1.1 发送者重试机制
修改publisher模块的application.yaml文件
java
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
1.2 发送者确认机制
RabbitMQ提供了生产者消息确认机制,包括Publisher Confirm 和Publisher Return 两种。在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执。

- 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
- 持久消息 投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
- 其它情况都会返回NACK,告知投递失败
其中ack和nack属于Publisher Confirm机制,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。
1.3 发送者确认机制代码实现
在publisher模块的application.yaml中添加配置:
java
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
这里publisher-confirm-type有三种模式可选:
- none:关闭confirm机制
- simple:同步阻塞等待MQ的回执
- correlated:MQ异步回调返回回执
1.3.1 定义ReturnCallback
每个RabbitTemplate只能配置一个ReturnCallback,在配置类中统一设置。
java
package com.itheima.publisher.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setReturnsCallback(returned -> {
log.error("监听到消息return callback");
log.debug("交换机:{}", returned.getExchange());
log.debug("routingkey:{}", returned.getRoutingKey());
log.debug("message:{}", returned.getMessage());
log.debug("replyCode:{}", returned.getReplyCode());
log.debug("replyText:{}", returned.getReplyText());
});
}
}
1.3.2 定义ConfirmCallback
java
@Test
public void testConfirmCallback() throws InterruptedException {
// 创建回调对象
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.debug("收到ConfirmCallback nack,消息发送失败!!! reason:{}", result.getReason());
}
}
});
// 队列名
String exchangeName = "amq.direct";
// 消息
String message = "yellow ! ! !";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "yellow", message, cd);
Thread.sleep(200);
}
这里的CorrelationData中包含两个核心的东西:
- id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆
- SettableListenableFuture:回执结果的Future对象

二. MQ的可靠性
2.1 数据持久化(一般都是已默认持久化)
交换机持久化

队列持久化

消息持久化

在开启持久化机制以后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。
不过出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化。一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,因此建议生产者确认全部采用异步方式。
2.2 LazyQueue
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
- 支持数百万条的消息存储
利用SpringAMQP声明队列的时候,添加x-queue-mod=lazy参数也可设置队列为Lazy模式:
java
@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy() // 开启Lazy模式
.build();
}
基于注解来声明队列并设置为Lazy模式:
java
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){
log.info("接收到 lazy.queue的消息:{}", msg);
}
三. 消费者的可靠性
3.1 消费者确认机制
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
- ack:成功处理消息,RabbitMQ从队列中删除该消息
- nack:消息处理失败,RabbitMQ需要再次投递消息
- reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
一般reject方式用的较少,除非是消息格式有问题,那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch机制捕获,消息处理成功时返回ack,处理失败时返回nack.
由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:
- none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
- auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
- 如果是业务异常,会自动返回nack;
- 如果是消息处理或校验异常,自动返回reject;
java
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 不做处理
3.2 消费者重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。
极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力
java
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
在消费者接收时故意添加一个异常

发送消息(不开启重试机制)

开启重试机制,性能会增强很多

3.3 消费失败处理策略
Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
定义一个RepublishMessageRecoverer,关联队列和交换机
java
package com.itheima.consumer.config;
import lombok.RequiredArgsConstructor;
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.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
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 republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
重试三次后自动传回到error.direct交换机

在error.direct中查看消息

四. 延迟消息
4.1 死信交换机
死信交换机是RabbitMQ中用于处理失败消息的机制。当消息满足某些条件无法被正常消费时,会被重新发送到另一个专门的交换机,这个交换机就是死信交换机。
消息成为死信的条件
- 消息被拒绝(basic.reject或basic.nack)且requeue=false
- 消息过期(TTL时间到)
- 队列达到最大长度,新消息无法进入

有一组绑定的交换机(ttl.fanout)和队列(ttl.queue)。但是ttl.queue没有消费者监听,而是设定了死信交换机hmall.direct,而队列direct.queue1则与死信交换机绑定,RoutingKey是blue。
定义死信交换机
java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dlx.queue"),
exchange = @Exchange(name = "dlx.direct", type = ExchangeTypes.DIRECT),
key = {"blue"}
))
public void listenDlxQueue(String message) {
log.info("消费者dlx.queue接收到消息:{}", message);
}
定义普通交换机,并绑定死信交换机
java
package com.itheima.consumer.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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("blue");
}
}
QueueBuilder.durable("normal.queue").deadLetterExchange("dlx.direct").build();
deadLetterExchange("死信交换机名字")
查看控制台


测试:向普通交换机normal.direct发送消息并设置消息过期时长为10秒
java
@Test
void testSendDelayMessage() {
rabbitTemplate.convertAndSend("normal.direct", "blue", "hello, spring amqp delay message", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
}
可以看到10秒后死信交换机的队列dlx.queue收到消息


4.2 延迟消息的插件
下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
文档: https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq
把下载好的文件放入你RabbitMQ挂载的数据卷下查看数据卷:docker volume inspect "你的mq"

执行安装插件
powershell
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

基于注解创建(添加一个delayed = "true")
java
@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);
}
基于Bean创建
java
package com.itheima.consumer.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class DelayExchangeConfig {
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayQueueBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
}
}

测试
java
@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;
}
});
}
可以看到刚好相差10秒


延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息。