JAVA高级工程师--RabbitMQ消息可靠性、若依集成升级

上上一篇博客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 方法发送消息。没有设置 CorrelationDataConfirmCallback 需要 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:高并发场景批量确认,提升性能 消费端业务处理(如订单处理、数据同步)

四、你一定需要确认机制吗?

那当然不是一定需要了啦。

必须使用的情况:

  1. 金融/支付场景

    • 转账、扣款、交易等

    • 消息丢失可能导致资金不一致

  2. 订单/库存系统

    • 订单创建、库存扣减

    • 消息丢失会导致超卖或少卖

  3. 重要通知/消息推送

    • 合同审批、重要公告

    • 必须确保消息送达

  4. 分布式事务补偿

    • Saga、TCC等分布式事务场景

    • 需要确保每个步骤都执行

  5. 对账/审计系统

    • 需要完整追踪消息流向

    • 法律合规要求

总之,

  • 涉及金钱、订单、库存

  • 有严格的数据一致性要求

  • 消息丢失无法通过其他手段恢复

五.需要确认机制的配置建议

场景1:普通日志/监控数据

java 复制代码
╔══════════════════╦═══════════╦══════════╦══════════╗
║    场景         ║ Publisher ║ Return   ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 日志收集         ║   可选    ║  不需要  ║  不需要  ║
║ 用户行为统计     ║   可选    ║  不需要  ║ 自动确认 ║
║ 非核心通知       ║   可选    ║  不需要  ║ 自动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝

场景2:一般业务数据

java 复制代码
╔══════════════════╦═══════════╦══════════╦══════════╗
║    场景         ║ Publisher ║ Return   ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 商品浏览记录     ║   推荐    ║  可选    ║ 手动确认 ║
║ 系统操作日志     ║   推荐    ║  可选    ║ 手动确认 ║
║ 缓存更新消息     ║   推荐    ║  可选    ║ 手动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝

之前若依集成版本就是一般业务的配置方式

场景3:核心业务数据(必须配置)

java 复制代码
╔══════════════════╦═══════════╦══════════╦══════════╗
║    场景         ║ Publisher ║ Return   ║ Consumer ║
╠══════════════════╬═══════════╬══════════╬══════════╣
║ 订单创建         ║   必须    ║   必须   ║ 手动确认 ║
║ 支付处理         ║   必须    ║   必须   ║ 手动确认 ║
║ 库存扣减         ║   必须    ║   必须   ║ 手动确认 ║
╚══════════════════╩═══════════╩══════════╩══════════╝

六.决策流程图

java 复制代码
开始
  │
  ↓
是否核心业务数据? → No → 使用简单配置(自动确认)
  │ Yes
  ↓
是否允许消息丢失? → Yes → 使用一般业务配置方式
  │ No
  ↓
是否允许重复消费? → No → 需要幂等性设计 + 核心业务配置方式
  │ Yes
  ↓
使用完整确认( 核心业务配置方式)
  │
  ↓
考虑添加:
  ├─ 消息持久化(durable=true)
  ├─ 生产者重试机制
  ├─ 消费者幂等性
  ├─ 死信队列
  └─ 监控告警

所以说,具体,怎么配置还是需要考量的。

相关推荐
人道领域1 小时前
javaWeb从入门到进阶(MyBatis拓展)
java·tomcat·mybatis
阿蒙Amon1 小时前
C#每日面试题-简述类型实例化底层过程
java·面试·c#
努力也学不会java2 小时前
【Spring Cloud】负载均衡-LoadBalance
java·人工智能·后端·spring·spring cloud·负载均衡
莫问前路漫漫2 小时前
Java static 与 final 详解(简单易懂)
java·开发语言
jiayong232 小时前
MQ性能优化面试题
java·性能优化·kafka·rabbitmq
明天…ling2 小时前
sql注入笔记总结
java·数据库·sql
独自破碎E2 小时前
【滑动窗口】最小覆盖子串
java·开发语言
好学且牛逼的马2 小时前
【手写Easy-Spring|1】
java·后端·spring