生产者消息丢失:–>生产者确认机制
- 消息未到达交换机 (成功ACK 不成功NACK)
- 消息到达交换机,未到达队列 (不成功ACK+报错)
MQ服务挂了,队列中的消息丢失 -->队列持久化( durable )
consumer接收消息但是未成功消费 -->消费者确认机制+消费者失败重试机制 (成功ACK 失败NACK)
spring:
rabbitmq:
host: wudw.top # rabbitMQ的ip地址
port: 5672 # 端口
username: wudw
password: wdwroot
virtual-host: /
publisher-confirm-type: correlated #消息结果处理 correlated异步回调->ConfirmCallBack simple 同步等待
publisher-returns: true #定义ReturnCallback
template:
mandatory: true #true 调用ReturnCallback ,false直接丢弃消息
ReturnCallback (全局只能定义一份)
@Configuration
@Slf4j
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//判断是否是延迟消息 如果是延迟队列中的消息 由队列获取消息在固定时间内不放行
//所以如果是延迟队列 会报错
if (message.getMessageProperties().getReceivedDelay()>0) {
//是延迟消息 忽略错误
return;
}
log.info("消息发送到队列失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
}
});
}
}
ConfirmCallBack(每个方法定义一份)
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(result -> {
if (result.isAck()) {
log.debug("消息发送成功到交换机,ID:{}", correlationData.getId());
} else {
log.error("消息发送到交换机失败,ID:{},,原因:{}", correlationData.getId(), result.getReason());
}
}, ex -> log.error("消息发送异常,id:{},原因:{}", correlationData.getId(), ex.getMessage())
);
Message message = MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setHeader("x-delay", 6000).build(); //设置为延迟消息 必须未mq配置延迟插件
rabbitTemplate.convertAndSend("delayExchange", "delay", message, correlationData);
log.info("发送延迟消息");
//睡一秒钟是因为消息发送是异步的,可能消息成功发送,但是主方法执行完了还没有收到返回,直接报错
Thread.sleep(1000);
创建持久的Exchange (默认就是持久的)
//第一种
ExchangeBuilder.directExchange("durableDirectExchange").durable(true).build();
//第二种
new DirectExchange("durableDirectExchange", true, false);
//第三种 注解方式 @Exchange -->delayed = “true”
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = "delayExchange",delayed = "true",durable = "true",type = ExchangeTypes.DIRECT),
value = @Queue(value = "delayQueue",durable = "true"),
key = {"delay"}
))
创建持久队列(默认就是持久的)
//1.第一种
QueueBuilder.durable("durableQueqe1").build();
//第二种 注解方式 @Queue -->delayed = “true”
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = "delayExchange",delayed = "true",durable = "true",type = ExchangeTypes.DIRECT),
value = @Queue(value = "delayQueue",durable = "true"),
key = {"delay"}
))
消息持久化(如果通过Darshbord 默认不持久化,代码 默认持久化)
//持久化(PERSISTENT) Delivery(传送)
Message message = MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。
而SpringAMQP则允许配置三种确认模式:
manual:手动ack,需要在业务代码结束后,调用api发送ack。
auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack。
(auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack)
spring.abbitmq.listener.simple.acknowledge-mode: auto # 关闭ack
开启本地重试:(默认最大重试后 直接丢弃消息)
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: auto #三种机制 NONE 直接丢弃 但是retry也可以用,MANUAL 编程式事务重试,AUTO SpringAOP自动重试
retry: #默认最大重试后 直接丢弃消息
enabled: true #开始重试机制
initial-interval: 1000 #重试间隔1000ms
multiplier: 2 #下次重试时间是上次重试间隔的几倍
max-attempts: 5 #最大重试次数
stateless: true #无状态 默认无状态 如果业务中有事务 一定要设为false false可以保留spring的事务
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:
/**
* 消息投递失败达到application.yml中retry指定次数后,存到errorQueue保存
* RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
* RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
* ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
*
* @param rabbitTemplate
* @return
*/
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
return new RepublishMessageRecoverer(rabbitTemplate, "errorExchange", "error");
}
总结
如何确保RabbitMQ消息的可靠性?
开启生产者确认机制,确保生产者的消息能到达队列
开启持久化功能,确保消息未消费前在队列中不会丢失
开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
消息是一个过期消息,超时无人消费
要投递的队列消息满了,无法投递
死信交换机和普通交换机类似,死信队列用来接收死信交换机中的消息。另外,队列将死信投递给死信交换机时,必须知道两个信息:
1. 死信交换机名称
2. 死信交换机与死信队列绑定的RoutingKey
可以给队列指定死信交换机,则该队列产生的死信都会发送给指定的死信交换机。
QueueBuilder.durable("ttlQueue")
.ttl(5000) //指定队列的过期时间
.deadLetterExchange("deadLetterExchange")
.deadLetterRoutingKey("dealLetter").build();
超时时间取以下两个中小的那个
1. 消息所在的队列设置了超时时间
2. 消息本身设置了超时时间
//消息设置超时时间
MessageBuilder.withBody(delay.getBytes(StandardCharsets.UTF_8))
.setExpiration(6000).build();
实现: 代码就不拷贝了 没啥新奇
1.创建队列 指定队列的过期时间
2.队列中的消息过期了 自动丢到死信交换机->死信队列
3.监听死信队列消息
虽然叫延迟队列 但是用的是DelayExchange
如果想使用延时队列:
docker cp /usr/wudw/rabbitmq/plugins/xxx rabbitmq:/plugins
docker exec -it rabbitmq bash
rabbitmq-plugins enable xxx
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之前发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。
解决消息堆积有两种思路:
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
@SpringAMQP声明lazy-queue
//第一种
QueueBuilder.lazy();
//第二种
@RabbitListener(queuesToDeclare=@Queue(
name="lazy.queue",
durable = "true",
arguments =@Argument(name="x-queue-mode",value="lazy")
))
public void listenLazyQueue(String msg){
xxxxxx
}
总结
消息堆积问题的解决方案?
- 队列上绑定多个消费者,提高消费速度
- 使用惰性队列,可以再mq中保存更多消息
惰性队列的优点有哪些?
- 基于磁盘存储,消息上限高
- 没有间歇性的page-out,性能比较稳定
惰性队列的缺点有哪些?
- 基于磁盘存储,消息时效性会降低
- 性能受限于磁盘的IO
普通集群 或者叫标准集群(classic cluster),具备下列特征:
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
镜像集群镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点
所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主
3.仲裁队列
仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致(集群是最终一致性 可能会造成数据丢失)
4. SpringAMQP
与其它配置唯一不同点:
spring:
rabbitmq:
addresses: ip1:port1,ip2:port2 #改成集群后 ,需要指定集群的iP:Port