消息可靠性问题
消息可靠性: 消息由生产者发送到MQ, 最终被消费者成功消费.
产生消息丢失的几种情况:
- 消息发送到交换机的过程中失败
- 消息成功发送到交换机,由交换机路由到队列的过程中失败
- MQ宕机, 队列中的消息丢失
- 消费者收到了消息, 在消费之前失败.
解决方法
生产者:
- 生产者无法连接MQ时, 开启重试策略,确保消息传入MQ,重试次数耗尽则存入数据库, 后期交给人工处理.
在配置文件中加入下列代码
yaml
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier
max-attempts: 3 # 总共尝试次数
- 生产者能够连接MQ, 但消息未到达交换机, 开启confirm机制, 消息到达交换机返回ack, 未到达交换机返回nack.
配置文件开启confirm机制
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
下列为测试代码: 当rabbitTemplate.convertAndSend("kk.direct", "red", "hello", cd);
中的kk.direct交换机存在,消息发送到交换机则收到ack否则收到nack.
java
@Test
void testPublisherConfirm() {
// 1.创建CorrelationData
CorrelationData cd = new CorrelationData();
// 2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("send message fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
log.debug("发送消息成功,到达交换机,收到 ack!");
}else{ // result.getReason(),String类型,返回nack时的异常描述
log.error("发送消息失败,没到交换机,收到 nack, reason : {}", result.getReason());
// 将发送失败的消息存储到数据库,将来尝试重发
}
}
});
// 3.发送消息
rabbitTemplate.convertAndSend("kk.direct", "red", "hello", cd);
}
- 消息到交换机但是没有到队列, 开启return机制,保证消息到达队列.
定义一个配置类开启return机制
java
package com.itheima.publisher.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
//路由到队列失败才执行到这里
log.error("触发return callback,");
log.debug("exchange: {}", returned.getExchange());
log.debug("routingKey: {}", returned.getRoutingKey());
log.debug("message: {}", returned.getMessage());
log.debug("replyCode: {}", returned.getReplyCode());
log.debug("replyText: {}", returned.getReplyText());
}
});
}
}
测试代码同上.
- 发送消息失败,如在ConfirmCallback中收到nack,ReturnCallBack异常, 可以将消息记录到失败消息表,由定时任务进行发布,每隔10秒钟(可设置)执行获取失败消息重新发送,发送一次则在失败次数字段加一,达到3次停止自动发送由人工处理。
消费者
消费者开启自动ack,当成功处理消息后向MQ发送ack,MQ从队列中删除消息,否则发送nack,MQ再次投递消息给消费者,如果是消息处理或校验异常 ,自动返回reject, 消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息.
这样做之后若是消费者出现异常,消息会不断被requeue到队列,可能会导致导致mq的消息处理飙升,带来不必要的压力. 可以设置本地重试策略,当本地重试次数耗尽后, 将失败消息路由到错误队列.
本地重试策略配置:
yaml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier
max-attempts: 3 # 最大重试次数
最后监听错误队列,把消息写入数据库,后期交由人工处理.
持久化操作
创建交换机、队列、消息时设置持久化,保证MQ宕机后重启不丢失.
交换机持久化:
队列持久化:
消息持久化:

消费积压问题
原因:
生产者生产消息的速度 远高于 消费者消费消息的速度,于是就会造成消息积压在MQ.
解决方法
- 可以采用惰性队列先将生产的消息存入磁盘, 缓解队列储存消息的压力, 在消费时再加载到内存.
不过这种方法可能会导致磁盘空间爆满. - 先分析消息积压的原因, 大概率消费者出问题,首先修复消费者代码,开启消费者程序,临时开启多个消费者,来以倍速消费积压的消息。当积压的消息消费的差不多的情况,关闭临时消费者.
消息重复消费问题
也是幂等性问题.
幂等性:在程序开发中,是指同一个业务,执行一次或多次对业务状态的影响是一致的. 如根据id删除数据,查询数据.
可是数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果,如取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况; 退款业务。重复退款对商家而言会有经济损失。
因此,我们必须想办法保证消息处理的幂等性.
解决方法
使用redis来实现
消费者先从redis缓存中获取消息ID,如果消息ID存在则说明有消费者正在消费该数据,则不执行任何操作, 若消息ID不存在,则说明这是第一次消费该数据,则正常消费,消费之后再redis中保存一个消息ID,并设置过期时间.