目录
[一、RabbitMQ可靠性其实是 三个阶段的问题:](#一、RabbitMQ可靠性其实是 三个阶段的问题:)
[3 发送失败重试(最多3次)](#3 发送失败重试(最多3次))
[1 队列持久化](#1 队列持久化)
[2 交换机持久化](#2 交换机持久化)
[3 消息持久化](#3 消息持久化)
[1 开启手动ACK](#1 开启手动ACK)
[2 消费者代码](#2 消费者代码)
[1 Redis去重(最常见)](#1 Redis去重(最常见))
[2 数据库唯一索引](#2 数据库唯一索引)
RabbitMQ是一个流行的开源消息代理,它提供了可靠的消息传递机制,广泛应用于分布式系统和微服务架构中。在现代应用中,确保消息的可靠性至关重要,以防止消息丢失和重复处理。本文将详细探讨RabbitMQ如何通过多种机制保证消息的可靠性,并提供相关的最佳实践。
一、RabbitMQ可靠性其实是 三个阶段的问题:
生产者 → MQ → 消费者
必须保证:
1 生产者发送消息不丢
2 MQ存储消息不丢
3 消费者消费消息不丢
因此完整方案:
生产者可靠投递
-
MQ持久化
-
消费者可靠消费
-
幂等控制
二、生产者如何保证消息可靠投递
生产者主要解决:
消息发送失败
消息未到达交换机
消息未路由到队列
需要使用两个机制:
Publisher Confirm
Return Callback
1.开启生产者确认机制
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
2.ConfirmCallback(确认是否到达交换机)
如果消息 没有到达交换机 会触发。
@Component
public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息成功到达交换机: " + correlationData.getId());
} else {
System.out.println("消息发送失败: " + cause);
// 重试机制
retrySend(correlationData);
}
}
}
3 发送失败重试(最多3次)
这个阶段是发送到交换机要重试;
public void retrySend(CorrelationData correlationData){
ReliableMessage msg = messageCache.get(correlationData.getId());
int retry = msg.getRetryCount();
if(retry < 3){
msg.setRetryCount(retry + 1);
rabbitTemplate.convertAndSend(
msg.getExchange(),
msg.getRoutingKey(),
msg.getMessage(),
new CorrelationData(msg.getId())
);
}else{
// 持久化数据库
failMessageService.save(msg);
}
}
4、ReturnCallback(防止消息路由失败)
场景:
交换机存在
但没有队列匹配 routingKey
rabbitTemplate.setReturnsCallback(returned -> {
System.out.println("消息路由失败");
System.out.println("exchange=" + returned.getExchange());
System.out.println("routingKey=" + returned.getRoutingKey());
});
三、MQ如何保证消息不丢失
MQ层面要解决:
RabbitMQ重启
服务器崩溃
需要 持久化机制。
1 队列持久化
@Bean
public Queue orderQueue(){
return new Queue("order.queue", true);
}
参数:
true = durable
2 交换机持久化
@Bean
public DirectExchange orderExchange(){
return new DirectExchange("order.exchange", true, false);
}
3 消息持久化
发送消息时设置:
MessageProperties props = new MessageProperties();
props.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
四、消费者如何保证消息不丢
消费者必须使用:
手动ACK
1 开启手动ACK
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
2 消费者代码
@RabbitListener(queues = "order.queue")
public void receive(Message message, Channel channel) throws Exception {
long tag = message.getMessageProperties().getDeliveryTag();
try {
String msg = new String(message.getBody());
System.out.println("收到消息: " + msg);
// 执行业务
process(msg);
// 手动确认
channel.basicAck(tag,false);
} catch (Exception e) {
// 消费失败
channel.basicNack(tag,false,true);
}
}
解释:
| 方法 | 作用 |
|---|---|
| basicAck | 确认消息 |
| basicNack | 拒绝消息 |
| requeue=true | 重新入队 |
五、如何防止消息重复消费
RabbitMQ保证:
至少消费一次
但不能保证:
只消费一次
所以必须做:
幂等控制
1 Redis去重(最常见)
public void process(String msg){
String msgId = getMsgId(msg);
if(redisTemplate.hasKey(msgId)){
return;
}
// 执行业务
createOrder(msg);
redisTemplate.opsForValue().set(msgId,"1");
}
2 数据库唯一索引
订单号 unique
ALTER TABLE orders ADD UNIQUE (order_no);
如果重复消费:数据库直接拒绝
六.消费消息失败写入死信队列
消费者消费消息失败有两种解决方式,一种是重试消费,另一种就是写入死信队列,死信队列的监听者来处理消息;
1.私信配置
1.1死信交换机
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange("dlx.exchange", true, false);
}
1.2死信队列
@Bean
public Queue deadLetterQueue() {
return new Queue("order.dlq", true);
}
1.3绑定死信队列
@Bean
public Binding deadLetterBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(deadLetterExchange())
.with("dlx.routingKey");
}
2.业务队列配置
关键配置:
x-dead-letter-exchange
x-dead-letter-routing-key
代码:
@Bean
public Queue orderQueue() {
Map<String, Object> args = new HashMap<>();
// 指定死信交换机
args.put("x-dead-letter-exchange", "dlx.exchange");
// 指定死信routingKey
args.put("x-dead-letter-routing-key", "dlx.routingKey");
return new Queue("order.queue", true, false, false, args);
}
3.消费代码
重点在:
basicNack(tag,false,false)
第三个参数 false 表示 不重新入队,MQ就会投递到死信队列。
@RabbitListener(queues = "order.queue")
public void receive(Message message, Channel channel) throws Exception {
long tag = message.getMessageProperties().getDeliveryTag();
try {
String msg = new String(message.getBody());
System.out.println("收到消息: " + msg);
// 模拟业务异常
int i = 1 / 0;
// 成功确认
channel.basicAck(tag, false);
} catch (Exception e) {
System.out.println("消费失败,进入死信队列");
// false = 不重新入队
channel.basicNack(tag, false, false);
}
}
4.死信队列消费者
死信队列可以专门监听:
@RabbitListener(queues = "order.dlq")
public void deadLetterConsumer(Message message) {
String msg = new String(message.getBody());
System.out.println("死信消息: " + msg);
// 可以做:
// 1 记录日志
// 2 存数据库
// 3 人工处理
}