Java连接RabbitMQ(SpringBoot版·下)

一:引言

通过《Java连接RabbitMQ(原生版)》一文中,我们了解了RabbitMQ的一些基本的使用方式,如交换机的创建及消息的投递,但是在企业中我们大部分都是把RabbitMQ集成到SpringBoot中的,所以原生的方式我们则不怎么使用到,下面我将和大家一起走入SpringBoot整合RabbitMQ的世界的下半部分。

<math xmlns="http://www.w3.org/1998/Math/MathML"> 本系列博文主要包含如下: \color{#f00}{本系列博文主要包含如下:} </math>本系列博文主要包含如下:

①:关于RabbitMQ部署和命令使用请参考: 彻底掌握RabbitMQ部署和操作(超详细)

②:使用官方自带Java API 连接RabbitMQ: Java连接RabbitMQ(原生版)

③:将RabbitMQ客户端集成进SpringBoot(上): Java连接RabbitMQ(SpringBoot版·上)

④:将RabbitMQ客户端集成进SpringBoot(下): Java连接RabbitMQ(SpringBoot版·下)
<math xmlns="http://www.w3.org/1998/Math/MathML"> 本文全部代码拉取地址 G i t e e : \color{#f00}{本文全部代码拉取地址Gitee:} </math>本文全部代码拉取地址Gitee:

本文给出的只是核心代码,完整代码请参考Gitee:所有案例完整代码
<math xmlns="http://www.w3.org/1998/Math/MathML"> 下面的所有示例只是核心代码,具体环境得参考完整案例代码。 \color{#f00}{下面的所有示例只是核心代码,具体环境得参考完整案例代码。} </math>下面的所有示例只是核心代码,具体环境得参考完整案例代码。

关于SpringBoot集成RabbitMQ,在上篇已经说明了基本的使用,如工作队列消息应答消息分发扇出、主题、直接交换机死信、延迟队列消息发布确认等等,而在这章主要讲解的是在实际开发中的一些问题的说明。比如最重要的幂等性问题啦。


二:幂等性(消息重复消费问题)

幂等性(Idempotency) 是指:无论同一条消息被消费多少次,最终业务系统的处理结果都是一致的,且不会产生副作用或重复影响。

在使用 RabbitMQ 进行消息通信的过程中,虽然在正常流程下消息投递和消费都应是一次性的,但由于系统的复杂性和分布式环境中的各种不可控因素,消息重复投递或重复消费是无法完全避免的现象 。因此,生产者和消费者都应做好幂等性处理,以保证业务逻辑的稳定性和一致性。
⭕可能导致重复消息的常见场景:

场景名称 原因描述 后果影响 典型触发时机
生产者重复投递 Web 接口被多次调用,每次调用均发送消息 RabbitMQ 接收到多条内容相同但消息 ID 不同的消息,导致消费者重复处理 前端多次点击、网关重试、接口幂等性缺失
消息队列中消息重复 消费者处理完成但 ACK 尚未发送,RabbitMQ 宕机或网络异常,ACK 丢失 RabbitMQ 认为消息未被确认,重启后重新投递该消息 RabbitMQ 重启、ACK 被拦截或超时
消费者处理异常或连接中断 消费者业务处理完成,但在 ACK 发送前宕机或断网 消息未确认,被 RabbitMQ 再次推送,导致重复消费 消费者故障、容器崩溃、网络断开

✅ Redis实现消息幂等性的通用方案说明(解决方式千千万,具体看自己项目):

为了解决消息重复投递与重复消费的问题,我采用了的是基于Redis的幂等性控制策略,核心思路是为每条消息设置一个全局唯一标识(如业务拼接生成的唯一ID或UUID),并结合缓存状态实现幂等校验。具体如下:

  • 生产者端处理逻辑: 在生产者投递消息前,首先将该消息的唯一标识写入Redis,并设置一个合理的过期时间。这个过期时间的作用是用于清理历史缓存记录,防止内存冗余,同时在一定时间窗口内避免重复投递。如果消息投递成功,则不需要手动删除该缓存项,等待其自动过期即可;如果投递失败,则需主动删除该缓存键,以便下次重试时能重新标记。
  • 消费者端处理逻辑: 消费者在接收到消息后,首先检查 Redis 中是否存在该唯一标识。如果标识不存在,说明是首次消费,则执行业务逻辑,并立即将该标识写入 Redis 表示"已处理"状态,同时设置相同的过期时间;如果标识已存在,则说明该消息已被处理或正在处理,直接跳过以防止重复消费操作。若业务处理过程中发生异常,需删除 Redis 中的该标识键,以允许后续重新处理。
  • 过期时间的设计说明: Redis 缓存键的过期时间应根据业务实际情况(如消息投递频率、消息生命周期)设置,通常为几分钟至几小时不等。其作用既是防止缓存堆积,也是避免历史重复消息误拦截新请求。

📝案例代码目录:

java 复制代码
./main/
├── java
│ └── cn
│     └── ant
│         ├── config
│         │ ├── RabbitMQConfig.java                 // 交换机和队列的创建和绑定关系
│         │ └── RabbitMQMyCallBack.java             // 关于发布确认的回调方法编写
│         ├── controller
│         │ └── TestController.java                 // 接收前端发送的任务消息
│         ├── entity
│         │ └── MessageSendDTO.java                 // 消息封装类
│         ├── IdempotentConsumerApplication.java
│         └── mqHandle
│             ├── ProducerSend.java                 // 消息生产者
│             └── QueueConsumer.java                // 普通消费者
└── resources
    ├── application.yml
    └── log4j2.xml

点开查看详情:关于生产者的幂等性处理

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class ProducerSend {
 
    // 注入rabbitTemplate对象
    private final RabbitTemplate rabbitTemplate;
    // 注入StringRedisTemplate对象
    private final StringRedisTemplate redisTemplate;
 
    /***
     * 生产者方法
     * @param msg 需要投递的消息
     */
    public void producerSendMsg(MessageSendDTO msg) {
        // Redis Key
        String key = "rabbit:confirm:order:" + msg.getMsgID();
        // redis的键值操作对象
        ValueOperations<String, String> forValue = redisTemplate.opsForValue();
        try {
            // 防止重复提交
            // 若设置成功则为True,设置不成功或者设置的值已经存在则返回False
            // 这里设置20秒代表自动过期,
            // 一旦设置这个键值,消息被成功投递则不删除(防止20秒内重复提交),但是投递失败
            // 以后我需要删除这个键值,方便下次继续设置投递;;具体按照实际设置过期时间
            Boolean deliver = forValue.setIfAbsent(key, String.valueOf(msg.getMsgID()),
                                20, TimeUnit.SECONDS);
 
            // 判断设置成功则发送消息(否则这个消息可能多次发送给消费者)
            if (Boolean.TRUE.equals(deliver)) {
                // 延迟测试(假设投递花了5秒),可以看到重复的问题演示
                Thread.sleep(5000);
                // 消息转换为JSON格式并转为字节数组
                byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
                // 设置回退的相关信息,并设置回调信息
                //(里面包含交换机和路由key,方便后面在回调的时候重新投递)
                CorrelationData cd = new CorrelationData();
                cd.setReturned(new ReturnedMessage(new Message(bytes), 0, null,
                        RabbitMQConfig.ORDINARY_DIRECT_EXCHANGE, RabbitMQConfig.ROUTE_KEY));
                // 发送消息
                rabbitTemplate.convertAndSend(RabbitMQConfig.ORDINARY_DIRECT_EXCHANGE,
                        RabbitMQConfig.ROUTE_KEY, bytes, cd);
            } else {
                log.info("消息已经由生产者发送投递了,请忽重复投递!");
            }
        } catch (Exception e) {
            // 若生产者投递出现问题则代表投递不成功,删除这次缓存
            redisTemplate.delete(key);
        }
    }
}

点开查看详情:关于消费者的幂等性处理

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class QueueConsumer {
 
    // 注入StringRedisTemplate对象
    private final StringRedisTemplate redisTemplate;
 
    /***
     * 消费者(监听队列ordinaryQueue)
     * @param msgData 传递的具体消息内容,最好是生产者发送使用什么类型,这里接收就用什么类型
     * @param deliveryTag 处理消息的编号,一般在消息确认时要使用
     * @param message 这个就类似我们原生的message
     * @param channel 这个就类似我们原生的channel
     * 关于@RabbitListener:只需要监听队列即可,多个则在{}里面逗号分割;ackMode确认模式
     */
    @RabbitListener(queues = {RabbitMQConfig.ORDINARY_QUEUE}, ackMode = "MANUAL")
    public void ordinaryQueueConsumption(@Payload String msgData,
                                         @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                                         Message message,
                                         Channel channel) throws IOException {
        // redis的键值操作对象
        ValueOperations<String, String> forValue = redisTemplate.opsForValue();
        // 获取到队列消息,因为发送是JSON格式,我们要解析对象格式
        // message.getBody():存储消息的具体内容(序列化后的二进制数据)
        String msgJsonStr = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageSendDTO msg = JSONObject.parseObject(msgJsonStr, MessageSendDTO.class);
        // Redis Key
        String key = "rabbit:consume:order:" + msg.getMsgID();
        try {
            // 判断消息有没有被消费过,没有消费过则设置(代表有消费者准备消费了)
            Boolean result = forValue.setIfAbsent(key,
                                String.valueOf(msg.getMsgID()), 1, TimeUnit.DAYS);
            // 判断,若设置成功代表可以消费此条消息
            if (Boolean.TRUE.equals(result)) {
                log.info("A:消息由消费者A消费:{},并消费完成", msg);
                // 手动确认,deliveryTag可以通过message.getMessageProperties().getDeliveryTag()拿到
                channel.basicAck(deliveryTag, false);
            } else {
                log.info("消费者当前消费的消息被别的消费者已经消费过了,或者正在消费:{}", msg);
                // 重复发送的也得手动确认掉,但是不处理
                channel.basicAck(deliveryTag, false);
            }
        } catch (Exception e) {
            // 若消费失败则删除之前的锁定(缓存),下次队列投递给消费者的时候可以继续消费
            redisTemplate.delete(key);
        }
    }
}

在实际项目中,为确保消息系统的高可用与可靠性,建议结合多种机制共同保障。例如,虽然基于 Redis 实现的幂等性控制方案可能不是最严谨或最通用的方式,但它在多数业务场景下已具备良好的实用性与扩展性,具有实施成本低、易于集成等优势,相关实现可参考 Gitee 上的开源示例代码。

为了实现整体消息流程的可靠交付,还必须正确配置 RabbitMQ 的消息确认机制(Publisher Confirm)。当消息发布到交换机失败时,需设置回调处理逻辑(如 ConfirmCallback)进行异常捕捉与补偿;同样,交换机投递到队列的过程中也存在返回机制(如 ReturnCallback),用于处理无法路由的消息。但需注意,如果配置了备份交换机(Alternate Exchange),将无法再触发 ReturnCallback,这时应结合备份逻辑做统一监控处理。

在消费者侧,即便实现了幂等性校验机制,也仍需启用消息确认模式(如手动 ACK),确保消费处理成功后再通知 MQ 服务端完成确认。否则在处理异常、断网或服务宕机等场景下,可能会导致重复投递。

综上所述,幂等性控制 + 消息发布确认机制 是保障消息系统稳定性和数据一致性的核心手段,应在系统设计初期予以完整考虑与实施。

还有就是,生产者投递的幂等性Key不要和消费者幂等性的Key一样,推荐使用这种方式来命名Redis的Key信息rabbit:[confirm|consume]:{业务名}:{messageId};否则会出现生产者投递了,但Key没到过期时间,在消费者设置Key时发现有了,那消费者会认为他已经消费了这条消息。

三:优先级队列

队列通常遵循 先进先出(FIFO)的消费顺序,即后投递的消息需等待前面消息被消费后才能处理。 但在实际业务中,常会遇到生产者投递速度远快于消费者处理速度的情况,导致队列中积压大量消息。如果此时有一条需紧急处理的高优先级消息被投递,它也会被阻塞在队列尾部,必须等待前面的消息依次消费完毕后才能处理,显然无法满足实时性要求。为解决此类问题,RabbitMQ提供了消息优先级队列(Priority Queue)机制,允许为不同消息设置优先级,从而实现高优先级消息优先出队并被消费,提升关键任务的响应速度和系统的灵活性。
🔊需要修改的代码片段:

java 复制代码
// RabbitMQConfig配置类修改: 
/**
 * 创建一个具备优先级支持的持久化队列。
 * 此队列允许消费者根据消息设置的优先级(priority)顺序进行消费。
 *
 * RabbitMQ 官方支持的优先级范围是 0~255。
 * 实际项目中建议使用 0~10 范围,优先级越高,越早被消费。
 *
 * ⚠️ 注意:
 * - 如果消息没有设置优先级,默认优先级为 0。
 * - 优先级设置过大,会造成调度开销增加,建议控制在合理范围内。
 * - 不是所有消息都应设置为高优先级,避免"优先级失效"。
 *
 * @return 创建后的优先级队列对象 Queue
 */
@Bean(value = "priorityQueueName")
public Queue createPriorityQueueName() {
    // 定义优先级队列的参数
    Map<String, Object> args = new HashMap<>();

    // 设置队列支持的最大优先级(官方支持 0~255,这里设置为 10)
    // 设置过大可能导致 RabbitMQ 内部排序耗费较多资源(性能问题)
    args.put("x-max-priority", 10);

    // 构建一个持久化(durable=true)、非独占(exclusive=false)、不自动删除的队列
    // 设置了 x-max-priority 参数以支持消息优先级
    return QueueBuilder
            .durable(PRIORITY_QUEUE_NAME)  // 队列名为 PRIORITY_QUEUE_NAME(应为常量)
            .withArguments(args)           // 设置参数
            .build();                      // 构建队列
}
java 复制代码
// 生产者代码修改: 
/**
 * 向优先级队列发送消息。
 * 其中第1005条消息被设定为优先级较高(priority=5),其余消息默认为priority=0。
 *
 * @param msg 消息数据实体(会被序列化为JSON字符串)
 */
public void sendMessage(MessageSendDTO msg) {
    // 模拟发送10条消息(从1001到1010)
    for (int i = 1001; i <= 1010; i++) {
        msg.setMsgID(i); // 设置消息唯一编号
        // 将消息对象转为 JSON 字节数组
        byte[] bytes = JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8);

        // 让第1005条消息拥有更高的优先级(优先被消费者处理)
        if (i == 1005) {
            // 创建消息属性对象,设置优先级
            MessageProperties messageProperties = new MessageProperties();
            messageProperties.setPriority(5); // 优先级范围通常为 0 ~ 10(必须与队列配置一致)
            
            // 构造完整的消息对象(包含内容和属性)
            Message message = new Message(bytes, messageProperties);
            // 发送到指定交换机和路由键
            rabbitTemplate.convertAndSend(
                    RabbitMQConfig.PRIORITY_DIRECT_EXCHANGE,RabbitMQConfig.PRIORITY_KEY,
                    message);
        } else {
            // 普通消息(无优先级,默认为0)
            rabbitTemplate.convertAndSend(
                    RabbitMQConfig.PRIORITY_DIRECT_EXCHANGE,RabbitMQConfig.PRIORITY_KEY,
                    bytes);
        }
    }

👀测试程序代码(具体代码请参考开头给出的Gitee代码):

我们需要注意的是RabbitMQ的优先级队列并不是一种严格意义上的"优先级抢占式调度"。其核心机制是在先进先出的基础上进行优先级排序。首先,优先级机制 只有在队列中存在多个待处理消息(即出现消息堆积)的情况下才会生效 ;如果消息一入队就立即被消费,优先级不会起作用。其次,RabbitMQ 默认支持 0 到 255 的优先级范围,但通常推荐设置较小的范围(如 0~10),以避免资源消耗过大。最后,消息在进入队列时是按发送顺序依次存放的,只有在队列中有多个待处理消息时,RabbitMQ 才会根据优先级对它们进行重新排序,从而实现优先处理高优先级消息的效果。

如上图的消费者稍微存在延迟,导致队列中出现消息积压,就会触发RabbitMQ的优先级重排序机制。当消费者消费速度较慢,消息1001到1004陆续进入队列但尚未被消费,随后消息1005也被投递并设置了较高的优先级(如优先级5),此时RabbitMQ发现队列中已有多个待处理消息,就会对这些消息根据优先级进行重新排序。于是,虽然1001是最早入队的消息,但由于已经在队列头部,可能会先被消费;接着,系统会优先处理优先级较高的1005,再继续按顺序消费1002、1003等消息。这个机制确保在消息堆积时,高优先级消息能够尽快得到处理。

注意 :对于重要或时效性强的业务,建议使用独立队列或延迟队列进行拆分处理 ,以实现更好的隔离性和可控性。同时,优先级队列虽然提供消息排序能力,但其调度性能相对较低,不适合作为核心高性能业务的主要调度机制,应谨慎使用。

四:惰性队列

从 RabbitMQ 3.6.0 ~ 3.11.0 中间的版本,官方引入了 惰性队列(Lazy Queue) 的机制。与默认的内存优先型队列不同,惰性队列的核心设计目标是:将尽可能多的消息直接存储到磁盘中,仅在消费者即将消费时才将消息加载进内存 。该机制特别适用于消息堆积严重、消费延迟较长 的业务场景。

在默认配置下,RabbitMQ 会尽可能将消息保留在内存中以提升吞吐性能,即使是标记为持久化的消息,也会在内存中保留副本。这种方式虽然高性能,但在消息量巨大或消费者宕机、下线、维护等场景下,容易导致内存压力剧增。而惰性队列则通过减少内存驻留量,显著降低了内存占用,增强了系统在高堆积压力下的稳定性和扩展性 。它有效避免了消息换页(paging)过程中可能出现的队列阻塞、性能抖动和新消息无法入队等问题。

需要注意的是,惰性队列在牺牲了一定的实时处理性能的同时,换取了更强的可堆积性和资源控制能力,因此适合用于不追求极致实时性但要求高可靠性或容错能力的场景。

java 复制代码
1️⃣:RabbitMQ支持两种队列模式:default(默认模式)和 lazy(惰性模式)
    Ⅰ:默认模式(default)
        1.在RabbitMQ 3.6.0版本之前,所有队列默认使用该模式。
        2.消息会尽可能存储在内存中,以提升处理速度。
        3.不需要额外配置。
    Ⅱ:惰性模式(lazy)
        1.从3.6.0 ~ 3.11 中间版本引入。
        2.消息默认存入磁盘,仅在消费者实际消费时才加载到内存中。
        3.优势在于:更高的消息堆积能力、减少内存压力,适用于消费者处理较慢或消息堆积较多的场景。

2️⃣:惰性队列支持以下两种配置方式
    Ⅰ:队列声明时设置:
        - 原生方式:
            使用channel.queueDeclare方法时,设置参数"x-queue-mode"为"lazy":
            Map<String, Object> args = new HashMap<>();
            args.put("x-queue-mode", "lazy");
            channel.queueDeclare("my-lazy-queue", true, false, false, args);
        - SpringBoot集成方式:
            Map<String, Object> args = new HashMap<>();
            args.put("x-queue-mode", "lazy");
            QueueBuilder.durable(ORDINARY_QUEUE).withArguments(args).build();
    Ⅱ:策略(Policy)方式设置:
        可通过RabbitMQ管理控制台或命令行设置队列模式的策略。
        若同时设置了声明参数与策略,策略(Policy)优先生效。
        比如执行如下命令:
            rabbitmqctl set_policy lazy-queue "^lazy-.*" '{"queue-mode":"lazy"}' --apply-to queues
        参数说明
            lazy-queue	            策略名称
            ^lazy-.*	            正则表达式,匹配所有以 lazy- 开头的队列
            {"queue-mode":"lazy"}	设置为惰性队列模式
            --apply-to queues	    表示应用于队列

(一):为啥移除惰性队列

<math xmlns="http://www.w3.org/1998/Math/MathML"> R a b b i t M Q 3.12 + 不再支持惰性队列? \color{#f00}{RabbitMQ3.12+不再支持惰性队列?} </math>RabbitMQ3.12+不再支持惰性队列?

是的!从RabbitMQ3.12开始,官方正式废弃了 x-queue-mode=lazy 参数及相关策略(如 "queue-mode": "lazy")。这意味着:即使你在队列创建时或策略中设置了该参数,RabbitMQ 也会默默忽略它,不会报错,但也不会起作用。 原因是:RabbitMQ 从 3.12 起引入了 统一的内存回收机制(paging system) ,用于替代手动设置的惰性队列逻辑。

<math xmlns="http://www.w3.org/1998/Math/MathML"> R a b b i t M Q 3.12 + 是如何处理消息堆积的? \color{#f00}{RabbitMQ3.12+是如何处理消息堆积的?} </math>RabbitMQ3.12+是如何处理消息堆积的?

内存状态 行为描述 特点 适用场景
✅ 内存压力较小时 队列消息保留在内存中,快速处理 高性能、低延迟 高吞吐、实时响应场景
✅ 内存占用上升(超过高水位线) RabbitMQ 自动将部分消息分页到磁盘(page-out) 减少内存占用,避免 OOM,无需人工干预 消息堆积、慢消费者、突发高峰场景

RabbitMQ 3.12+ 采用自动调度和动态分页机制,既保留了小量消息的内存高性能优势,又在内存压力过大时自动落盘,无需手动区分惰性队列,简化配置同时避免因误用x-queue-mode带来的性能与稳定性问题。 关于解决堆积的阈值配置设置说明点这里

相关推荐
趙卋傑22 分钟前
Spring MVC
java·开发语言·后端·spring·mvc
hqxstudying25 分钟前
J2EE模式---服务定位器模式
java·开发语言·后端·python·spring·java-ee
德育处主任33 分钟前
亚马逊云科技玩法:用 S3 + CloudFront,给你的静态网站上个全球 CDN 加速 ✈️
服务器·后端·cdn
FLYINGPIG38 分钟前
【开源软件】SimpleAI一款轻量级的桌面随身AI助手
后端·llm
花落人散处40 分钟前
SpringAI——完成 Function Calling
java·后端·openai
酸奶小肥阳40 分钟前
Spring AI Alibaba 学习(一):入门初体验
后端
用户214118326360244 分钟前
免费玩转顶尖代码生成!魔搭社区 + Qwen3-Coder+Claude Code 全攻略
后端
bobz9651 小时前
类型断言 vmiObj.(*v1.VirtualMachineInstance)
后端
陈随易1 小时前
Vite和pnpm都在用的tinyglobby文件匹配库
前端·后端·程序员
知行小栈1 小时前
牛马的人生,需要Spring Shell
java·后端·spring