RabbitMQ高级篇:消息可靠性、幂等性与延迟消息

RabbitMQ 高级篇:消息可靠性、幂等性与延迟消息

整理自黑马程序员《SpringCloud微服务开发与实战》项目

对应课程章节:MQ高级(消息可靠性、LazyQueue、死信交换机、延迟插件)


一、消息可靠性:不丢消息的三道屏障

消息丢失可能发生在生产者 → MQ → 消费者的任何一个环节,解决方案需层层设防。

1. 生产者可靠性(发送端确认)

问题:网络抖动导致消息未到达交换机,或路由键错误导致消息无法投递到队列。

解决方案 :开启 Publisher ConfirmPublisher Return 机制。

配置与代码实现

yaml 复制代码
spring:
  rabbitmq:
    # 1. 开启确认机制(correlated:异步回调,推荐)
    publisher-confirm-type: correlated
    # 2. 开启回退机制(消息无法路由到队列时返回)
    publisher-returns: true
    template:
      mandatory: true  # 必须设为true,ReturnCallback才生效
java 复制代码
@Slf4j
@Configuration
public class MqConfirmConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        
        // 设置ReturnCallback(路由失败回调)
        rabbitTemplate.setReturnsCallback(returned -> {
            log.error("消息路由到队列失败,交换机:{},路由键:{},消息:{}", 
                     returned.getExchange(), returned.getRoutingKey(), returned.getMessage());
            // 可在此处记录日志或重发消息
        });
        
        // 设置ConfirmCallback(消息到达交换机回调)
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.debug("消息发送到交换机成功,ID:{}", correlationData.getId());
            } else {
                log.error("消息发送到交换机失败,ID:{},原因:{}", correlationData.getId(), cause);
            }
        });
    }
}

注意 :发送消息时需传入 CorrelationData(包含唯一ID),用于在回调中区分消息。

2. MQ可靠性(数据持久化)

问题:MQ宕机导致内存中的消息丢失。

解决方案:将消息、队列、交换机全部持久化到磁盘。

组件 持久化配置(SpringAMQP默认行为) 说明
交换机 @Exchange(durable = "true") SpringAMQP 默认 durable=true
队列 @Queue(durable = "true") 必须显式声明,否则重启MQ队列消失
消息 MessageDeliveryMode.PERSISTENT SpringAMQP 默认已设置

生产建议 :直接使用 Lazy Queue(惰性队列),它是解决消息堆积和持久化的终极方案。

  • 原理:消息直接写入磁盘,消费时才加载到内存。
  • 配置 :在队列声明时设置 x-queue-mode: lazy,或通过管理界面设置。

3. 消费者可靠性(消费端确认)

问题:消费者拿到消息后,业务处理失败或消费者宕机,导致消息丢失。

解决方案 :采用 ACK 确认机制 + 失败重试策略

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto  # 自动ACK(业务执行成功自动确认,异常则拒绝)
        retry:
          enabled: true         # 开启消费者重试
          max-attempts: 3       # 最大重试次数
          initial-interval: 1000ms # 重试间隔

失败处理策略

  • RejectAndDontRequeueRecoverer(默认):重试耗尽后,丢弃消息(慎用)。
  • RepublishMessageRecoverer(推荐):重试耗尽后,将消息投递到指定的"异常交换机",人工介入处理。

二、消费者幂等性:防止重复消费

问题根源:网络抖动导致 MQ 未收到 ACK,消息被重新投递,造成业务重复执行(如:扣款两次)。

解决方案"业务判断"优于"消息ID去重"

1. 业务状态判断法(推荐)

在处理消息前,先查询当前业务状态。以"支付成功"消息为例:

java 复制代码
@Transactional
public void handlePaySuccess(Long orderId) {
    // 1. 查询当前订单状态
    Order order = orderMapper.selectById(orderId);
    
    // 2. 幂等判断:只有未支付状态的订单才处理
    if (order != null && order.getStatus() == OrderStatus.UNPAID) {
        // 执行业务:更新为已支付
        order.setStatus(OrderStatus.PAID);
        orderMapper.updateById(order);
    } else {
        // 已支付过,直接确认消息(幂等)
        log.info("订单已处理,跳过重复消费,订单ID:{}", orderId);
    }
}

2. 唯一约束/Redis防重

  • 数据库唯一索引:利用数据库主键或业务唯一键(如:支付流水号)防止重复插入。
  • Redis Token :消费前执行 setnx(key, 1),若已存在则说明已消费。

三、延迟消息:订单超时未支付取消

业务场景:用户下单后30分钟未支付,自动取消订单并释放库存。

方案一:死信交换机(DLX)+ TTL(不推荐,仅作了解)

原理:设置一个"缓冲队列"(无消费者),并给该队列设置 TTL(过期时间)和死信交换机。消息过期后变成"死信",自动转发到真正的业务队列。

缺陷

  • 队列级 TTL:队列中所有消息的延迟时间必须一致。
  • 灵活性差:无法实现"前一条消息延迟10s,后一条延迟1h"的场景。

方案二:RabbitMQ 延迟插件(推荐)

步骤

  1. 安装插件 :在 RabbitMQ 容器中执行 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  2. 声明延迟交换机 :类型必须为 x-delayed-message,并指定路由模式(如 direct)。
  3. 发送消息 :在 MessageProperties 中设置 setDelay(毫秒)

代码实现

java 复制代码
// 1. 声明延迟交换机(注解方式)
@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "order.delay.queue", durable = "true"),
    exchange = @Exchange(
        name = "order.delay.exchange", 
        type = "x-delayed-message", // 关键:使用延迟插件类型
        arguments = @Argument(name = "x-delayed-type", value = "direct")
    ),
    key = "order.delay"
))

// 2. 发送延迟消息(下单时)
public void sendDelayMessage(Long orderId) {
    rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", orderId, message -> {
        // 设置延迟时间(30分钟)
        message.getMessageProperties().setDelay(30 * 60 * 1000);
        return message;
    });
}

// 3. 监听延迟队列(处理超时订单)
@RabbitListener(queues = "order.delay.queue")
public void cancelOrder(Long orderId) {
    // 查询订单状态,若未支付则取消
    orderService.cancelOrderIfUnpaid(orderId);
}

四、黑马商城业务改造总结

业务场景 可靠性保障 关键配置/代码
支付成功通知 生产者确认 + 消费者幂等 publisher-confirm-type: correlated + 订单状态判断
下单清理购物车 消息持久化 + LazyQueue 队列声明 durable=true,使用惰性队列防堆积
超时订单取消 延迟插件 + 幂等性 x-delayed-message 交换机,防止重复取消

最佳实践口诀

  • 消息必持久:队列、交换机、消息全部持久化。
  • 确认不能少:生产端 Confirm,消费端 ACK。
  • 幂等靠业务:不要依赖消息ID,直接判断业务状态。
  • 延迟用插件 :死信队列已过时,rabbitmq_delayed_message_exchange 是首选。
相关推荐
A-Jie-Y26 分钟前
JAVA框架-SpringBoot环境搭建指南
java·spring boot
深兰科技34 分钟前
深兰科技与淡水河谷合作推进:矿区示范加速落地
java·人工智能·python·c#·scala·symfony·深兰科技
码界奇点1 小时前
基于Spring Boot的前后端分离商城系统设计与实现
java·spring boot·后端·java-ee·毕业设计·源代码管理
一叶飘零_sweeeet1 小时前
深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解
java·死锁·活锁·饥饿
IT乐手1 小时前
java 对比分析对象是否有变化
android·java
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【18】Hook 接口和四大抽象类
java·人工智能·spring
Hachi被抢先注册了1 小时前
Docker学习记录
java·云原生·eureka
devilnumber2 小时前
Spring Boot 2 vs Spring Boot 3:50 条核心区别 + 升级优势 + 避坑指南
java·spring boot·springboot升级
武超杰2 小时前
Spring Cloud Alibaba Nacos 进阶:配置隔离、集群、持久化与开机自启
java·开发语言
Venhoul2 小时前
@Scheduled(cron = “1 0 0 * * ?“用法介绍
java