上上一篇博客JAVA高级工程师-消息中间件RabbitMQ(一)
https://blog.csdn.net/heni6560/article/details/156765816?spm=1011.2124.3001.6209
提到过如何保证 RabbitMQ 消息不丢失?(可靠性),本次专门深入讲解,并且与实际开发工作结合,以及还要与这篇结合来说
JAVA高级工程师-若依项目集成消息中间件RabbitMQ(三)
https://blog.csdn.net/heni6560/article/details/157216885?spm=1011.2124.3001.6209
像与钱相关的系统,涉及钱相关的业务使用rabbitMQ时就对消息可靠性要求很高了。
一、逻辑图

confirm、return都是基于生产端的确认机制。
1. Confirm 机制(生产者 → Broker)--生产端
-
作用:确认消息是否成功到达 RabbitMQ Broker
-
时机 :消息发送到交换机时触发,这个消息不管是否成功,这个回调函数一定会执行
-
确认对象:生产者 → RabbitMQ 服务器
2. Return 机制(Broker → 生产者) 回退模式--生产端
-
作用:当消息无法路由到队列时,返回给生产者
-
时机 :消息从交换机路由到队列失败时触发,只有失败才会执行这个函数。
-
前提 :必须设置
mandatory=true
3. Ack 机制(消费者 → Broker)--消费者
-
作用:确认消息是否被成功消费
-
时机 :消费者处理完消息后触发,结果会处理失败或处理成功。
-
确认对象:消费者 → RabbitMQ 服务器
ack的意思是Acknowledge,确认的意思,表示消费者收到消息后的确认,可以理解为这是一个信息的回执
ack机制有三种确认方式:
- 自动确认: Acknowledge=none(默认)
- 消费者收到消息,则会自动给broker一个回执,表示消息收到了,但是消费者里的监听方法内部是我们自己的业务,业务是否成功,他不管的,如果业务出现问题出现异常,那么也就相当于这条消息是失败的。
- 这就相当于我寄了一个快递,对方收到后快递公司就自动确认了快递的签收确认,这是很常见的手段吧。一旦快递内部是否破损我们就不知道了,对吧。
- 手动确认:Acknowledge=manual
- 消费端收到消息后,不会通知broker回执,需要等待我们自己的业务处理完毕OK了没问题,然后手动写一条代码去确认,当然如果出现错误异常,我们可以有兜底的处理方法,比如记录日志或者重新发新的消息都行。
- 根据异常类型确定:Acknowledge=auto
- 消费端处理业务出现不同的异常,根据异常的类型来做出不同的响应处理,这种方式比较麻烦,需要提前预判很多异常类型。这里了解一下有这个类型即可。
确认机制的作用对比
| 机制 | 作用方向 | 主要目的 | 使用场景 |
|---|---|---|---|
| Confirm | 生产者 → Broker | 防止消息发送丢失 | 1. 金融交易 2. 订单创建 3. 重要通知 |
| Return | Broker → 生产者 | 处理无法路由的消息 | 1. 动态路由 2. 配置检查 3. 监控告警 |
| Ack | 消费者 → Broker | 确保消息成功消费 | 1. 数据一致性 2. 防止重复消费 3. 错误重试 |
二、若依项目集成RabbitMq
https://blog.csdn.net/heni6560/article/details/157216885?spm=1011.2124.3001.6209
1.confirm、return确认机制
该篇集成rabbiltMq,在配置yml中:
publisher-confirms: true # 开启Confirm确认机制
publisher-returns: true # 开启Return确认机制
但是代码层面,是没有实现生产者端确认机制的,
应该在RabbitTemplate中设置以下回调:
java
// Confirm回调 - 消息是否到达Exchange
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息发送到Exchange成功: {}", correlationData);
} else {
log.error("消息发送到Exchange失败: {}, cause: {}", correlationData, cause);
}
});
// Return回调 - 消息是否路由到Queue
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息路由到Queue失败: exchange={}, routingKey={}, replyCode={}, replyText={}",
exchange, routingKey, replyCode, replyText);
});
在RabbitMqConfig中添加配置:
java
@Configuration
@Slf4j
public class RabbitMqConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true); // 开启Return回调
// 设置Confirm回调
template.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息发送到Exchange成功, correlationData: {}", correlationData);
} else {
log.error("消息发送到Exchange失败, correlationData: {}, cause: {}",
correlationData, cause);
// 这里可以进行重试或其他补偿操作
}
});
// 设置Return回调
template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息无法路由到Queue, 被返回. exchange={}, routingKey={}, replyCode={}, replyText={}",
exchange, routingKey, replyCode, replyText);
// 这里可以记录失败消息,进行后续处理
});
return template;
}
// ... 其他配置 ...
}
或者,在RabbitMqClient中添加回调
在RabbitMqClient的构造函数中添加:
java
@Autowired
public RabbitMqClient(RabbitAdmin rabbitAdmin, RabbitTemplate rabbitTemplate) {
this.rabbitAdmin = rabbitAdmin;
this.rabbitTemplate = rabbitTemplate;
// 设置Confirm回调
this.rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息投递到Exchange成功: {}", correlationData);
} else {
log.error("消息投递到Exchange失败: {}, 原因: {}", correlationData, cause);
// 消息投递失败处理逻辑
}
});
// 设置Return回调
this.rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息无法路由到Queue,被退回. exchange={}, routingKey={}, replyCode={}, replyText={}",
exchange, routingKey, replyCode, replyText);
// 消息退回处理逻辑
});
}
2.ack确认机制:
yml
java
listener:
simple:
acknowledge-mode: manual # 手动确认模式
代码实现:
在 BaseRabbiMqHandler.java 中:
java
public void onMessage(T t, Long deliveryTag, Channel channel, MqListener mqListener) {
try {
// 业务处理...
mqListener.handler(t, channel);
// ⭐⭐⭐ 这里显式调用了basicAck进行消息确认 ⭐⭐⭐
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.info("接收消息失败,重新放回队列");
try {
// ⭐⭐⭐ 这里调用了basicNack进行消息拒绝并重新入队 ⭐⭐⭐
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
RabbitMqConfig中的配置:
java
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
// ⭐⭐⭐ 设置为手动确认 ⭐⭐⭐
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// ... 其他配置
}
3.之前代码存在的问题和风险:
a) BaseRabbiMqHandler 中的问题:
java
public class BaseRabbiMqHandler<T> {
// ❌ 这里提前获取了token,在多线程环境下可能有问题
private String token = UserTokenContext.getToken();
public void onMessage(T t, Long deliveryTag, Channel channel, MqListener mqListener) {
try {
UserTokenContext.setToken(token); // 恢复token
mqListener.handler(t, channel);
channel.basicAck(deliveryTag, false); // 确认消息
} catch (Exception e) {
// ❌ 这里总是重新入队,可能造成无限重试
channel.basicNack(deliveryTag, false, true);
}
}
}
我在刚开始调试前,有错误,就是不停地重试,这些重试的消息其实是不再需要的。
b) RabbitMqConfig 中的容器监听器:
java
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.debug("收到消息,deliveryTag: {}", deliveryTag);
try {
// ⚠️ 这里缺少实际的消息处理逻辑!
// 没有调用任何handler,也没有进行ACK
} catch (Exception e) {
// ⚠️ 只有异常时才NACK,正常流程没有ACK
channel.basicNack(deliveryTag, false, false);
}
}
});
所以,进行了以下的改进:
(1)增加根据异常类型是否重新入队.
java
@Slf4j
public class BaseRabbiMqHandler<T> {
public void onMessage(T t, Long deliveryTag, Channel channel, MqListener mqListener) {
try {
// 业务处理
mqListener.handler(t, channel);
// 确认消息
channel.basicAck(deliveryTag, false);
log.debug("消息处理成功并确认,deliveryTag: {}", deliveryTag);
} catch (Exception e) {
log.error("消息处理失败,deliveryTag: {}, 错误: {}", deliveryTag, e.getMessage(), e);
// 根据异常类型决定是否重新入队
boolean requeue = shouldRequeue(e);
try {
channel.basicNack(deliveryTag, false, requeue);
if (requeue) {
log.warn("消息重新入队,deliveryTag: {}", deliveryTag);
} else {
log.error("消息被丢弃,deliveryTag: {}", deliveryTag);
// 可以记录到死信队列或数据库
}
} catch (IOException ex) {
log.error("NACK操作失败", ex);
}
}
}
private boolean shouldRequeue(Exception e) {
// 根据异常类型判断是否重新入队
// 例如:业务异常可以重试,数据格式错误不应重试
return !(e instanceof IllegalArgumentException || e instanceof DataFormatException);
}
}
(2)confirm、return配置问题及解决
但是你写了上面那些,你运行起来,会发现
出现的第一个问题:"不论对错都该进入的confirm却都没进",为啥呢?
在 RabbitMqClient.java 中,使用的是 convertAndSend 方法发送消息。没有设置 CorrelationData!ConfirmCallback 需要 CorrelationData 才能工作。
java
// 在 RabbitMqClient.java 的 sendMessage 方法中修改
public void sendMessage(String queueName, Object params) {
log.info("发送消息到mq");
try {
// 创建 CorrelationData,可以包含消息ID等信息
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(
cc.wj.rabbitmq.exchange.DelayExchangeBuilder.DELAY_EXCHANGE,
queueName,
params,
message -> message,
correlationData // 添加这个参数
);
} catch (Exception e) {
e.printStackTrace();
}
}
// 对于延迟消息方法也要修改
private void send(String queueName, Object params, Integer expiration) {
// ... 前面的绑定代码不变
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(
cc.wj.rabbitmq.exchange.DelayExchangeBuilder.DEFAULT_DELAY_EXCHANGE,
queueName,
params,
message -> {
if (expiration != null && expiration > 0) {
message.getMessageProperties().setHeader("x-delay", expiration);
}
return message;
},
correlationData // 添加这个参数
);
}
但是呢,最后还是不对,后面在
RabbitMqConfig中的rabbitTemplate方法:
java
// 重要:确认这是 CachingConnectionFactory
if (connectionFactory instanceof CachingConnectionFactory) {
CachingConnectionFactory ccf = (CachingConnectionFactory) connectionFactory;
// 开启发布者确认
ccf.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
// 开启返回模式
ccf.setPublisherReturns(true);
}
增加这个代码生效了,那这也说明,yml:
java
# ❌ 错误的配置(旧版本)
spring:
rabbitmq:
publisher-confirms: true # Spring Boot 2.x 已废弃
publisher-returns: true # Spring Boot 2.x 已废弃
需要改成:
java
# ✅ 正确的配置(Spring Boot 2.x+)
spring:
rabbitmq:
publisher-confirm-type: correlated # CORRELATED, SIMPLE, 或 NONE
publisher-returns: true

第二个问题,延时队列,发送消息会进入ReturnCallback ,触发条件是:消息无法立即路由到队列。
所以我为延迟消息和非延迟消息使用不同的 RabbitTemplate
java
package cc.wj.rabbitmq.config;
import cc.wj.rabbitmq.core.MapMessageConverter;
import cc.wj.rabbitmq.event.WjRemoteApplicationEvent;
import com.rabbitmq.client.Channel;
import com.ruoyi.common.security.filter.TransmitUserTokenFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* 消息队列配置类
*
* @author zyf
*/
@Configuration
@RemoteApplicationEventScan(basePackageClasses = WjRemoteApplicationEvent.class)
@Slf4j
public class RabbitMqConfig {
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
//设置忽略声明异常
rabbitAdmin.setAutoStartup(true);
rabbitAdmin.setIgnoreDeclarationExceptions(true);
return rabbitAdmin;
}
@Bean
@Primary
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true); // 开启Return回调
// 设置Confirm回调:成功与否,都要调
template.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息发送到Exchange成功, correlationData: {}", correlationData);
} else {
log.error("消息发送到Exchange失败, correlationData: {}, cause: {}",
correlationData, cause);
// 这里可以进行重试或其他补偿操作
}
});
// 设置Return回调:路由到队列失败调
template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息无法路由到Queue, 被返回. exchange={}, routingKey={}, replyCode={}, replyText={}",
exchange, routingKey, replyCode, replyText);
// 这里可以记录失败消息,进行后续处理
});
return template;
}
// 延迟消息的 RabbitTemplate(关闭 ReturnCallback 或特殊处理)
@Bean("delayRabbitTemplate")
public RabbitTemplate delayRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(false); // ❗重要:关闭 mandatory,不触发 ReturnCallback
// Confirm 回调还是要的
template.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("延迟消息到达Exchange: {}", correlationData != null ? correlationData.getId() : "未知");
} else {
log.error("延迟消息发送失败: {}", cause);
}
});
return template;
}
@Bean // 没有指定 name,默认方法名就是 Bean 名
public MapMessageConverter mapMessageConverter() {
return new MapMessageConverter();
}
/**
* 注入获取token过滤器
*
* @return
*/
@Bean
public TransmitUserTokenFilter transmitUserInfoFromHttpHeader() {
return new TransmitUserTokenFilter();
}
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//手动确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//当前的消费者数量
container.setConcurrentConsumers(1);
//最大的消费者数量
container.setMaxConcurrentConsumers(1);
//是否重回队列
// container.setDefaultRequeueRejected(true);
// //消费端的标签策略
// container.setConsumerTagStrategy(new ConsumerTagStrategy() {
// @Override
// public String createConsumerTag(String queue) {
// return queue + "_" + UUID.randomUUID().toString();
// }
// });
container.setDefaultRequeueRejected(false);
// 设置预取数量为1,避免消息积压
container.setPrefetchCount(1);
// 设置并发消费者
container.setConcurrentConsumers(1);
container.setMaxConcurrentConsumers(1);
// 设置错误处理器
container.setErrorHandler(t -> {
log.error("消息监听容器发生错误: {}", t.getMessage(), t);
});
// 重要:设置消息监听适配器
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.debug("收到消息,deliveryTag: {}", deliveryTag);
try {
// 这里应该调用您的业务处理逻辑
// 注意:需要根据实际情况传递 deliveryTag 和 channel
} catch (Exception e) {
// 手动拒绝消息,不重新入队
channel.basicNack(deliveryTag, false, false);
}
}
});
return container;
}
}
RabbitMqClient.java 中:
java
@Configuration
public class RabbitMqClient {
@Autowired
private RabbitTemplate rabbitTemplate; // 普通消息模板
@Autowired
@Qualifier("delayRabbitTemplate")
private RabbitTemplate delayRabbitTemplate; // 延迟消息模板
// 修改 send 方法,使用延迟消息模板
private void send(String queueName, Object params, Integer expiration) {
Queue queue = new Queue(queueName);
addQueue(queue);
CustomExchange customExchange = cc.wj.rabbitmq.exchange.DelayExchangeBuilder.buildExchange();
rabbitAdmin.declareExchange(customExchange);
Binding binding = BindingBuilder.bind(queue).to(customExchange).with(queueName).noargs();
rabbitAdmin.declareBinding(binding);
CorrelationData correlationData = new CorrelationData("delay_" + System.currentTimeMillis());
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("发送延迟消息: 队列={}, 延迟={}ms, 时间={}", queueName, expiration, sf.format(new Date()));
// ❗使用延迟消息模板发送
delayRabbitTemplate.convertAndSend(
cc.wj.rabbitmq.exchange.DelayExchangeBuilder.DEFAULT_DELAY_EXCHANGE,
queueName,
params,
message -> {
if (expiration != null && expiration > 0) {
message.getMessageProperties().setHeader("x-delay", expiration);
}
return message;
},
correlationData
);
}
// 普通消息仍然使用普通模板
public void sendMessage(String queueName, Object params) {
CorrelationData correlationData = new CorrelationData("normal_" + System.currentTimeMillis());
rabbitTemplate.convertAndSend(
cc.wj.rabbitmq.exchange.DelayExchangeBuilder.DELAY_EXCHANGE,
queueName,
params,
message -> message,
correlationData
);
}
}
问题解决。
(3)在多线程环境下的线程安全
BaseRabbiMqHandler中的token存储
java
public class BaseRabbiMqHandler<T> {
// ❌ 成员变量在多线程实例间共享,可能导致token错乱
private String token = UserTokenContext.getToken();
}
-
Bean初始化时机:这个赋值发生在Spring Bean初始化时(单例模式)
-
token来源 :初始化时的
UserTokenContext.getToken()可能是null或者某个测试用户的token -
实际运行时 :所有消息处理都会使用同一个token,而不是发送消息时的真实用户token
这个类是消费者继承使用的。那不同消费者类不同token,但是同一个消费者不同线程还是相同的。token在Bean初始化时 确定,而不是在消息发送时确定,无法传递真实用户的身份信息。
java
package cc.wj.rabbitmq.core;
import com.rabbitmq.client.Channel;
import com.ruoyi.common.security.filter.UserTokenContext;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.DataFormatException;
/**
* @author zyf
*/
@Slf4j
public class BaseRabbiMqHandler<T> {
// ❌ 在Bean初始化时获取token,成员变量在多线程实例间共享,可能导致token错乱。
// private String token = UserTokenContext.getToken();
private final Map<Long, Integer> retryCountMap = new ConcurrentHashMap<>();
private static final int MAX_RETRY_COUNT = 3;
public void onMessage(T t, Long deliveryTag, Channel channel, cc.wj.rabbitmq.listenter.MqListener mqListener) {
String token = UserTokenContext.getToken();
try {
UserTokenContext.setToken(token);
mqListener.handler(t, channel);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.info("接收消息失败,重新放回队列");
// 根据异常类型决定是否重新入队
boolean requeue = shouldRequeue(e);
try {
// channel.basicNack(deliveryTag, false, requeue);
/**
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
* requeue:被拒绝的是否重新入队列
*/
channel.basicNack(deliveryTag, false, requeue);
if (requeue) {
log.warn("消息重新入队,deliveryTag: {}", deliveryTag);
} else {
log.error("消息被丢弃,deliveryTag: {}", deliveryTag);
// 可以记录到死信队列或数据库
}
} catch (IOException ex) {
log.error("NACK操作失败", ex);
}
}
}
private boolean shouldRequeue(Exception e) {
// 根据异常类型判断是否重新入队
// 例如:业务异常可以重试,数据格式错误不应重试
return !(e instanceof IllegalArgumentException || e instanceof DataFormatException);
}
}
(3)重试,之后在死信队列再说吧。
4.yml配置文件(最终版)
java
spring:
cloud:
bus:
enabled: true
destination: springCloudBus # 使用固定队列名
stream:
bindings:
springCloudBusInput:
destination: springCloudBus # 固定输入通道
springCloudBusOutput:
destination: springCloudBus
amqp:
# 仅开发模式使用
deserialization.trust.all: true
rabbitmq:
addresses: ip
port: 5672
username: guest
password: ENC(GbYmN1PAEshLs8IRBXiiGg==)
virtual-host: /
# 心跳配置(重要!)60秒心跳
requested-heartbeat: 60
connection-timeout: 10000
# Spring Boot 2.x+
# CORRELATED, SIMPLE, 或 NONE
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual
#消费者的最小数量
concurrency: 1
#消费者的最大数量
max-concurrency: 1
#是否支持重试
retry:
enabled: true
jasypt:
encryptor:
#秘钥明文是不行的哈,等于没加密
password: wj
algorithm: PBEWithMD5AndDES
代码一直上传不上去,最近还没空处理。
三、消息确认后,然后呢?
消息确认机制不仅仅是打日志 ,而是为了保证消息的可靠性 和可追溯性。
| 确认 类型 | 核心确认信息 | 可执行的核心操作(除日志外) | 适用业务场景 |
|---|---|---|---|
| Confirm生产者确认 | 1. 消息唯一标识(deliveryTag) 2. 是否成功(ack/nack) 3. 失败原因(如 Exchange 不存在) | 1. 失败重试:nack 时按策略重试(指数退避、固定间隔) 2. 死信存储:重试失败后存入数据库 / Redis,后续人工补偿 3. 监控告警:失败次数阈值触发邮件 / 钉钉告警 4. 链路追踪:记录确认状态到分布式追踪系统(如 SkyWalking) 5. 业务状态更新:成功则标记消息 "已发送",失败标记 "发送失败" | 金融交易、 订单通知、 核心数据同步 |
| Return消息回退 | 1. 消息体 / 属性 2. 回退原因(如路由键不匹配、Queue 不存在) 3. 原 Exchange / 路由键 | 1. 路由修复:检查路由规则,自动修正路由键后重发 2. 死信队列转发:转发到通用死信 Queue,后续人工分析 3. 业务降级:触发备用业务流程(如短信 + 人工补单) 4. 配置校验:检测 Exchange / 路由键配置是否异常,自动告警 | 多队列路由、 动态路由配置场景 |
| Ack 消费者确认 | 1. 消息唯一标识(deliveryTag) 2. 确认类型(ack/nack/reject) 3. 重入队列标记(requeue) | 1. 正常 ack:更新业务状态(如订单 "已处理")、清理待处理缓存 2. nack/reject(不重入):消息入死信队列 + 标记业务 "处理失败"+ 触发补偿 3. nack(重入):限制重入次数,超过则走失败流程 4. 批量 ack:高并发场景批量确认,提升性能 | 消费端业务处理(如订单处理、数据同步) |
四、你一定需要确认机制吗?
那当然不是一定需要了啦。
必须使用的情况:
-
金融/支付场景
-
转账、扣款、交易等
-
消息丢失可能导致资金不一致
-
-
订单/库存系统
-
订单创建、库存扣减
-
消息丢失会导致超卖或少卖
-
-
重要通知/消息推送
-
合同审批、重要公告
-
必须确保消息送达
-
-
分布式事务补偿
-
Saga、TCC等分布式事务场景
-
需要确保每个步骤都执行
-
-
对账/审计系统
-
需要完整追踪消息流向
-
法律合规要求
-
总之,
-
涉及金钱、订单、库存
-
有严格的数据一致性要求
-
消息丢失无法通过其他手段恢复
五.需要确认机制的配置建议
场景1:普通日志/监控数据
java
╔══════════════════╦═══════════╦══════════╦══════════╗
║ 场景 ║ Publisher ║ Return ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 日志收集 ║ 可选 ║ 不需要 ║ 不需要 ║
║ 用户行为统计 ║ 可选 ║ 不需要 ║ 自动确认 ║
║ 非核心通知 ║ 可选 ║ 不需要 ║ 自动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝
场景2:一般业务数据
java
╔══════════════════╦═══════════╦══════════╦══════════╗
║ 场景 ║ Publisher ║ Return ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 商品浏览记录 ║ 推荐 ║ 可选 ║ 手动确认 ║
║ 系统操作日志 ║ 推荐 ║ 可选 ║ 手动确认 ║
║ 缓存更新消息 ║ 推荐 ║ 可选 ║ 手动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝
之前若依集成版本就是一般业务的配置方式
场景3:核心业务数据(必须配置)
java
╔══════════════════╦═══════════╦══════════╦══════════╗
║ 场景 ║ Publisher ║ Return ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 订单创建 ║ 必须 ║ 必须 ║ 手动确认 ║
║ 支付处理 ║ 必须 ║ 必须 ║ 手动确认 ║
║ 库存扣减 ║ 必须 ║ 必须 ║ 手动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝
六.决策流程图
java
开始
│
↓
是否核心业务数据? → No → 使用简单配置(自动确认)
│ Yes
↓
是否允许消息丢失? → Yes → 使用一般业务配置方式
│ No
↓
是否允许重复消费? → No → 需要幂等性设计 + 核心业务配置方式
│ Yes
↓
使用完整确认( 核心业务配置方式)
│
↓
考虑添加:
├─ 消息持久化(durable=true)
├─ 生产者重试机制
├─ 消费者幂等性
├─ 死信队列
└─ 监控告警
所以说,具体,怎么配置还是需要考量的。