Spring Boot + RabbitMQ 实战:消息可靠投递+防重复消费(可直接落地)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

Spring Boot + RabbitMQ 实战:消息可靠投递+防重复消费(可直接落地)

在高并发业务场景中,RabbitMQ 作为消息中间件,核心作用是削峰填谷、解耦服务,但最关键的两个问题的是:消息不丢失、不重复消费。

基于 Spring Boot 整合 RabbitMQ,提供一套可直接复制、生产环境可用的实战代码,涵盖「生产者 Confirm 确认、消息持久化、消费者手动 ACK、Redis 幂等防重」全流程,避开所有常见坑,新手也能快速落地。

适用场景:订单异步创建、短信/通知推送、物流状态同步等所有需要保证消息可靠性的业务,尤其适配高并发下单、秒杀等场景。

一、核心需求与技术选型

1. 核心需求(必满足)

  • 生产者:消息必须送达 RabbitMQ Broker,失败可重试,杜绝生产者丢消息

  • 消息本身:Broker 重启、服务宕机后,消息不丢失

  • 消费者:业务处理成功后再确认消息,异常可重新入队,杜绝消费端丢消息

  • 幂等性:避免因网络重试、消息重入队导致的重复消费(比如重复创建订单、重复扣库存)

2. 技术选型

  • 框架:Spring Boot 2.x(兼容 3.x,只需微调依赖)

  • 消息中间件:RabbitMQ 3.9+

  • 幂等校验:Redis(高效判重)+ 数据库唯一索引(兜底)

  • 核心依赖:spring-boot-starter-amqp、spring-boot-starter-data-redis

二、环境配置(application.yml)

核心配置:开启生产者 Confirm 机制、Return 机制,消费者手动 ACK,同时配置限流防止数据库被冲垮,注释清晰可直接复制。

yaml 复制代码
spring:
  # RabbitMQ 核心配置
  rabbitmq:
    host: 127.0.0.1  # 本地环境,生产环境替换为服务器地址
    port: 5672       # RabbitMQ 默认端口
    username: guest  # 默认用户名,生产环境需修改为自定义账号
    password: guest  # 默认密码,生产环境需修改
    virtual-host: /  # 虚拟主机,默认即可
    connection-timeout: 10000  # 连接超时时间,避免无限等待
    # 1. 生产者确认机制:确保消息到达 Broker
    publisher-confirm-type: correlated  # correlated:异步回调,获取确认结果
    # 2. 消息回退机制:消息无法路由时返回生产者,避免消息丢失
    publisher-returns: true
    # 3. 消费者配置
    listener:
      simple:
        acknowledge-mode: manual  # 手动 ACK(关键!避免自动确认丢消息)
        concurrency: 5            # 消费者核心并发数
        max-concurrency: 10       # 消费者最大并发数
        prefetch: 10              # 限流:每次只获取10条消息,防止消费过快冲垮数据库

三、核心代码实现(全可复制)

1. 依赖导入(pom.xml)

无需额外配置,导入 Spring Boot 整合 RabbitMQ 和 Redis 的 starter 即可。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--  lombok 简化代码,可选但推荐 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. 实体类(OrderCreateDTO)

订单消息传输的实体类,根据自身业务调整字段,需实现 Serializable 接口(RabbitMQ 消息传输要求)。

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 订单创建消息DTO
 */
@Data
public class OrderCreateDTO implements Serializable {
    // 订单唯一编号(用于业务幂等)
    private String orderSn;
    // 用户ID
    private Long userId;
    // 订单金额
    private BigDecimal orderAmount;
    // 商品ID(多个可改为List)
    private Long productId;
    // 购买数量
    private Integer quantity;
}

3. 生产者:消息可靠投递(Confirm + 持久化)

核心逻辑:

  • 生成全局唯一 msgId(用于后续幂等判重)

  • 设置消息持久化(Broker 重启后消息不丢失)

  • 开启 Confirm 回调,监听消息是否成功送达 Broker,失败可重试/落库

java 复制代码
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;

/**
 * 订单消息生产者(可靠投递)
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderProducer {

    // 注入RabbitTemplate,用于发送消息
    private final RabbitTemplate rabbitTemplate;

    // 交换机名称(需与消费者队列绑定)
    private static final String ORDER_EXCHANGE = "order.exchange";
    // 路由键(需与队列绑定,确保消息能路由到指定队列)
    private static final String ORDER_CREATE_ROUTING_KEY = "order.create";

    /**
     * 发送订单创建消息
     * @param dto 订单创建DTO
     */
    public void sendOrderMsg(OrderCreateDTO dto) {
        // 1. 生成全局唯一消息ID,用于幂等判重(UUID保证唯一性)
        String msgId = UUID.randomUUID().toString().replace("-", "");
        // 2. 关联消息ID,用于Confirm回调获取消息标识
        CorrelationData correlationData = new CorrelationData(msgId);

        // 3. 发送消息(设置持久化 + 携带消息ID)
        rabbitTemplate.convertAndSend(
                ORDER_EXCHANGE,          // 交换机
                ORDER_CREATE_ROUTING_KEY,// 路由键
                dto,                     // 消息内容
                message -> {
                    // 设置消息持久化(MessageDeliveryMode.PERSISTENT:持久化)
                    message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                    // 将msgId存入消息属性,供消费者获取
                    message.getMessageProperties().setMessageId(msgId);
                    return message;
                },
                correlationData          // 关联消息ID,用于Confirm回调
        );

        // 4. 生产者Confirm回调:监听消息是否成功送达Broker
        rabbitTemplate.setConfirmCallback((correlation, ack, cause) -> {
            // 获取回调的消息ID
            String msgIdCallback = correlation.getId();
            if (ack) {
                // ack为true:消息成功送达Broker
                log.info("消息发送成功,msgId:{}", msgIdCallback);
            } else {
                // ack为false:消息发送失败
                log.error("消息发送失败,msgId:{},失败原因:{}", msgIdCallback, cause);
                // 失败处理:可重试发送(建议最多3次),或入库定时重发(避免消息丢失)
                retrySendMsg(dto, msgIdCallback);
            }
        });
    }

    /**
     * 消息发送失败重试(简单重试逻辑,可根据业务优化)
     */
    private void retrySendMsg(OrderCreateDTO dto, String msgId) {
        int retryCount = 3; // 重试3次
        for (int i = 0; i < retryCount; i++) {
            try {
                Thread.sleep(1000 * (i + 1)); // 指数退避重试(1s、2s、3s)
                CorrelationData correlationData = new CorrelationData(msgId);
                rabbitTemplate.convertAndSend(
                        ORDER_EXCHANGE,
                        ORDER_CREATE_ROUTING_KEY,
                        dto,
                        message -> {
                            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                            message.getMessageProperties().setMessageId(msgId);
                            return message;
                        },
                        correlationData
                );
                log.info("消息重试发送成功,msgId:{},重试次数:{}", msgId, i + 1);
                return;
            } catch (Exception e) {
                log.error("消息重试发送失败,msgId:{},重试次数:{}", msgId, i + 1, e);
                if (i == retryCount - 1) {
                    // 重试3次仍失败,入库定时重发(此处省略入库逻辑,可结合定时任务实现)
                    log.error("消息重试3次失败,msgId:{},已入库待定时重发", msgId);
                }
            }
        }
    }
}

4. 消费者:手动 ACK + Redis 幂等防重

核心逻辑:

  • 手动 ACK:业务处理成功后,调用 basicAck 确认消息;异常则调用 basicNack 重新入队

  • Redis 幂等:用 setIfAbsent 存储 msgId,已消费则直接 ACK,避免重复消费

  • 业务兜底:结合数据库唯一索引,防止 Redis 挂了导致的重复消费

java 复制代码
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
import java.util.concurrent.TimeUnit;

/**
 * 订单消息消费者(手动ACK + 幂等防重)
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderConsumer {

    // 注入Redis模板,用于幂等判重
    private final StringRedisTemplate redisTemplate;
    // 注入订单服务,处理核心业务逻辑
    private final OrderService orderService;

    // 队列名称(需与交换机、路由键绑定)
    private static final String ORDER_CREATE_QUEUE = "order.create.queue";
    // Redis 幂等键前缀(区分不同业务的消息)
    private static final String MQ_CONSUMED_KEY_PREFIX = "mq:consumed:order:";

    /**
     * 消费订单创建消息
     * @param dto 消息内容(自动反序列化)
     * @param message 消息对象,用于获取msgId
     * @param channel 信道对象,用于手动ACK/NACK
     */
    @RabbitListener(queues = ORDER_CREATE_QUEUE) // 监听指定队列
    public void consumeOrderMsg(OrderCreateDTO dto, Message message, Channel channel) throws Exception {
        // 1. 获取消息ID(生产者存入的msgId)
        String msgId = message.getMessageProperties().getMessageId();
        // 2. 获取消息投递标签(用于手动ACK/NACK)
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        // 3. Redis 幂等判重:setIfAbsent 原子操作,避免并发重复消费
        String redisKey = MQ_CONSUMED_KEY_PREFIX + msgId;
        // 存入Redis,有效期24小时(根据业务调整,确保消息消费完成后不会被重复判断)
        Boolean consumeFlag = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 24, TimeUnit.HOURS);

        // 4. 已消费过:直接ACK,避免重复处理
        if (consumeFlag == null || !consumeFlag) {
            log.warn("消息已消费,无需重复处理,msgId:{}", msgId);
            // 手动ACK:deliveryTag为当前消息标签,false表示不批量确认
            channel.basicAck(deliveryTag, false);
            return;
        }

        try {
            // 5. 处理核心业务逻辑:创建订单、扣减库存等
            orderService.createOrder(dto);

            // 6. 业务处理成功:手动ACK,通知RabbitMQ删除消息
            channel.basicAck(deliveryTag, false);
            log.info("消息消费成功,msgId:{},订单号:{}", msgId, dto.getOrderSn());

        } catch (Exception e) {
            log.error("消息消费异常,msgId:{},订单号:{}", msgId, dto.getOrderSn(), e);

            // 7. 异常处理:根据异常类型决定是否重入队
            // 可重试异常(如网络波动、数据库临时不可用):重入队(third参数为true)
            // 不可重试异常(如业务校验失败、参数错误):直接拒绝,不重入队(third参数为false)
            if (isRetryException(e)) {
                log.info("消息消费异常(可重试),将重入队,msgId:{}", msgId);
                channel.basicNack(deliveryTag, false, true);
            } else {
                log.info("消息消费异常(不可重试),直接拒绝,msgId:{}", msgId);
                // 不可重试异常:拒绝消息,不重入队(可结合死信队列处理)
                channel.basicReject(deliveryTag, false);
            }
        }
    }

    /**
     * 判断是否为可重试异常(根据自身业务调整)
     */
    private boolean isRetryException(Exception e) {
        // 示例:网络异常、数据库异常可重试,业务异常不可重试
        return e instanceof RuntimeException
                && (e.getMessage().contains("网络") || e.getMessage().contains("数据库"));
    }
}

5. 业务层:Redis 幂等 + 数据库兜底

**核心:**即使 Redis 挂了,通过数据库唯一索引(order_sn)兜底,确保不会重复创建订单、重复扣库存。

java 复制代码
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 订单服务(核心业务逻辑)
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderService {

    private final OrderMapper orderMapper;
    private final ProductMapper productMapper;

    /**
     * 创建订单(带幂等校验)
     * @param dto 订单创建DTO
     */
    @Transactional(rollbackFor = Exception.class) // 事务管理,异常回滚
    public void createOrder(OrderCreateDTO dto) {
        String orderSn = dto.getOrderSn();
        // 1. 数据库幂等兜底:查询订单是否已存在(订单表order_sn字段建唯一索引)
        Integer orderCount = orderMapper.countByOrderSn(orderSn);
        if (orderCount > 0) {
            log.warn("订单已存在,无需重复创建,订单号:{}", orderSn);
            return;
        }

        // 2. 核心业务逻辑:创建订单、扣减库存(根据自身业务实现)
        // ① 扣减商品库存(需加锁,避免超卖,此处省略分布式锁逻辑)
        Product product = productMapper.selectById(dto.getProductId());
        if (product == null || product.getStock() < dto.getQuantity()) {
            throw new RuntimeException("商品不存在或库存不足,订单号:" + orderSn);
        }
        product.setStock(product.getStock() - dto.getQuantity());
        productMapper.updateById(product);

        // ② 插入订单记录
        Order order = new Order();
        order.setOrderSn(orderSn);
        order.setUserId(dto.getUserId());
        order.setOrderAmount(dto.getOrderAmount());
        order.setProductId(dto.getProductId());
        order.setQuantity(dto.getQuantity());
        order.setStatus(0); // 0:待支付
        orderMapper.insert(order);

        log.info("订单创建成功,订单号:{}", orderSn);
    }
}

6. 交换机、队列绑定(可选,两种方式)

方式1:代码绑定(推荐,部署时自动创建,无需手动操作)

java 复制代码
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 交换机、队列绑定配置
 */
@Configuration
public class RabbitMQConfig {

    // 交换机名称(与生产者、消费者一致)
    public static final String ORDER_EXCHANGE = "order.exchange";
    // 队列名称(与消费者一致)
    public static final String ORDER_CREATE_QUEUE = "order.create.queue";
    // 路由键(与生产者一致)
    public static final String ORDER_CREATE_ROUTING_KEY = "order.create";

    // 1. 声明交换机(direct类型,持久化)
    @Bean
    public DirectExchange orderExchange() {
        // durable=true:交换机持久化,重启后不丢失
        return new DirectExchange(ORDER_EXCHANGE, true, false);
    }

    // 2. 声明队列(持久化)
    @Bean
    public Queue orderCreateQueue() {
        // durable=true:队列持久化;exclusive=false:不排他;autoDelete=false:不自动删除
        return new Queue(ORDER_CREATE_QUEUE, true, false, false);
    }

    // 3. 绑定交换机、队列、路由键
    @Bean
    public Binding orderCreateBinding() {
        return BindingBuilder.bind(orderCreateQueue())
                .to(orderExchange())
                .with(ORDER_CREATE_ROUTING_KEY);
    }
}

方式2:RabbitMQ 管理界面手动绑定(适合测试环境,生产环境推荐代码绑定)

  • 登录 RabbitMQ 管理界面(默认地址:http://localhost:15672

  • 创建交换机:类型 direct,名称 order.exchange,勾选 durable

  • 创建队列:名称 order.create.queue,勾选 durable

  • 绑定:交换机 → 队列,路由键填写 order.create

四、关键知识点与避坑点(实战重点)

  1. 消息不丢失的3个关键
  • 生产者:开启 Confirm 机制,确保消息到达 Broker,失败重试/落库

  • 消息:设置持久化(MessageDeliveryMode.PERSISTENT),交换机、队列也需持久化

  • 消费者:手动 ACK,业务成功后再确认,异常合理处理(重入队/死信)

  1. 防重复消费的2层保障
  • 第一层:Redis setIfAbsent 原子操作(高效判重,适合高并发)

  • 第二层:数据库唯一索引(兜底,防止 Redis 挂了导致的重复消费)

  1. 常见坑及解决方案
  • 坑1:消息自动 ACK → 解决方案:配置 acknowledge-mode: manual,手动 ACK

  • 坑2:消息未持久化 → 解决方案:设置消息、交换机、队列均为持久化(durable=true)

  • 坑3:Redis 挂了导致重复消费 → 解决方案:数据库唯一索引兜底

  • 坑4:消费者并发过高冲垮数据库 → 解决方案:配置 prefetch 限流,控制每次获取的消息数

  • 坑5:消息发送失败后不重试 → 解决方案:实现 Confirm 回调,失败后指数退避重试,重试失败入库定时重发

五、测试验证(快速验证可用性)

  1. 启动 RabbitMQ 服务(本地可通过 Docker 快速部署)

  2. 启动 Spring Boot 项目,自动创建交换机、队列并绑定

  3. 编写测试类,调用 OrderProducer 的 sendOrderMsg 方法发送消息

  4. 查看日志:消息发送成功 → 消费成功 → 订单创建成功

  5. 测试异常场景:关闭数据库,发送消息,查看是否重入队;恢复数据库后,查看是否正常消费

  6. 测试重复消费:手动将消息重新入队,查看是否会重复创建订单(应提示"订单已存在")

六、总结

本文提供的代码的是生产环境真实落地版本,涵盖了 RabbitMQ 消息可靠投递和防重复消费的全流程,无需修改核心逻辑,只需根据自身业务调整实体类和业务方法,即可快速集成到项目中。

核心思路:生产者靠 Confirm 保送达,消息靠持久化保存活,消费者靠手动 ACK 保消费,幂等靠 Redis+数据库保唯一,四者结合,彻底解决 RabbitMQ 消息丢失和重复消费的痛点。

后续可优化方向 :消息重试机制(结合定时任务)、死信队列(处理不可重试异常消息)、分布式锁(防止库存超卖),可根据业务复杂度逐步迭代。

相关推荐
gelald18 小时前
SpringBoot - Actuator与监控
java·spring boot·后端
我登哥MVP18 小时前
【Spring6笔记】 - 11 - JDBCTemplate
java·数据库·spring boot·mysql·spring
希望永不加班19 小时前
SpringBoot 自定义 Starter:从零开发一个私有 Starter
java·spring boot·后端·spring·mybatis
悟空码字19 小时前
别再System.out了!这份SpringBoot日志优雅指南,让你告别日志混乱
java·spring boot·后端
一 乐19 小时前
工会管理|基于springboot + vue工会管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·工会管理系统
ffqws_20 小时前
Spring Boot:用JWT令牌和拦截器实现登录认证(含测试过程和关键注解讲解)
java·spring boot·后端
8Qi820 小时前
RabbitMQ高级篇:消息可靠性、幂等性与延迟消息
java·分布式·微服务·中间件·rabbitmq·springcloud
yxl_num21 小时前
Docker 完整部署一个包含 Spring Boot(依赖 JDK)、MySQL、Redis、Nginx 的整套服务
java·spring boot·docker
一只幸运猫.21 小时前
用户58856854055的头像[特殊字符]Spring Boot 多模块项目中 Parent / BOM / Starter 的正确分工
java·spring boot·后端