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 是首选。
相关推荐
段ヤシ.9 小时前
回顾Java知识点,面试题汇总Day17(持续更新)
java·springboot·spring security·shiro·mybatis-plus·jdbctemplate·spring data jpa
jjjava2.09 小时前
Java 多线程核心基础与线程安全
java·开发语言
水木流年追梦9 小时前
大模型入门-大模型优化方法3
人工智能·分布式·python·深度学习·机器学习
147API9 小时前
Claude Opus 4.8 接口与工程落地分析:长任务调用链应该怎么设计
java·前端·数据库
lulu12165440789 小时前
Claude钩子系统架构设计:从执行时序到扩展机制
java·人工智能·python·ai编程
AI行业学习9 小时前
CC-Switch 下载、安装与使用配置指南【2026.5.29】
java·开发语言·vscode·python·eclipse·laravel
Regentsoft丽晶软件9 小时前
传统单体架构拖垮分销效率:2026品牌分销系统微服务化升级的价值拆解
微服务·云原生·架构
是狐狸吖9 小时前
Redis分布式锁进阶第十六篇
数据库·redis·分布式
许彰午9 小时前
03_Java流程控制详解
java·开发语言·python
逻极9 小时前
Go 从入门到精通:并发编程与云原生实践
微服务·云原生·go·并发