JAVA高级工程师--RabbitMQ消费者消息限流、超时、死信队列以及若依集成升级

一、RabbitMQ消费者消息限流

在一开始介绍MQ的时候,就提到了削峰填谷,本质上就是限流,所以我们需要对限流做一个落地的实现。

限流主要在消费者这一块,基于消费者做代码实现。并且基于手动ack的开启

yml:

复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 2        # 每个消费者最多2条未确认消息
        concurrency: 1     # 固定1个消费者
        max-concurrency: 1 # 最多也只有1个消费者

我配置了,但是并未生效。后面发现代码中硬编码了,其实那段

复制代码
RabbitMqConfig类中该方法messageListenerContainer是不需要的,在
复制代码
RabbitMqClient类中并未使用SimpleMessageListenerContainer
复制代码
那需要yml配置怎么生效的呢?

在这个包中,

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.amqp;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

@ConfigurationProperties(
    prefix = "spring.rabbitmq"
)
public class RabbitProperties {
    private static final int DEFAULT_PORT = 5672;
    private static final int DEFAULT_PORT_SECURE = 5671;
    private static final int DEFAULT_STREAM_PORT = 5552;
    private String host = "localhost";
    private Integer port;
    private String username = "guest";
    private String password = "guest";
    private final Ssl ssl = new Ssl();
    private String virtualHost;
    private String addresses;
    private AbstractConnectionFactory.AddressShuffleMode addressShuffleMode;
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration requestedHeartbeat;
    private int requestedChannelMax;
    private boolean publisherReturns;
    private CachingConnectionFactory.ConfirmType publisherConfirmType;
    private Duration connectionTimeout;
    private Duration channelRpcTimeout;
    private final Cache cache;
    private final Listener listener;
    private final Template template;
    private final Stream stream;
    private List<Address> parsedAddresses;

    public RabbitProperties() {
        this.addressShuffleMode = AddressShuffleMode.NONE;
        this.requestedChannelMax = 2047;
        this.channelRpcTimeout = Duration.ofMinutes(10L);
        this.cache = new Cache();
        this.listener = new Listener();
        this.template = new Template();
        this.stream = new Stream();
    }

    public String getHost() {
        return this.host;
    }

    public String determineHost() {
        return CollectionUtils.isEmpty(this.parsedAddresses) ? this.getHost() : ((Address)this.parsedAddresses.get(0)).host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public Integer getPort() {
        return this.port;
    }

    public int determinePort() {
        if (CollectionUtils.isEmpty(this.parsedAddresses)) {
            Integer port = this.getPort();
            if (port != null) {
                return port;
            } else {
                return (Boolean)Optional.ofNullable(this.getSsl().getEnabled()).orElse(false) ? 5671 : 5672;
            }
        } else {
            return ((Address)this.parsedAddresses.get(0)).port;
        }
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public String getAddresses() {
        return this.addresses;
    }

    ......
}
java 复制代码
RabbitProperties.SimpleContainer simple = rabbitProperties.getListener().getSimple();
Integer concurrency = simple.getConcurrency();
Integer prefetch = simple.getPrefetch();
log.info("【我是处理人1】 prefetch : " + prefetch);
log.info("【我是处理人1】 concurrency : " + concurrency);

打印的配置也正是yml配置的,所以按照要求,放心进行配置,会自动载入配置。

二、RabbitMQ ttl特性控制短信队列超时

假设现在短信发送量很多,消息太多太多了,可能处理不过来,那么假设短信验证码本身就是5分钟内失效的,但是5分钟过后还没有发出去,mq消息还是在队列中没有被消费者消费,那么这条消息其实是作废的,没有用的。而且本身用户已经等了那么久了都没收到,所以我们能不能索性设定一个时间为60s,60秒还没有消费消息,那么就不消费了呗,让用户再次发送一个请求完事。

可以使用ttl这个特性。

  • TTL:time to live 存活时间,和redis的ttl是一个道理
  • 如果消息到到ttl的时候,还没有被消费,则自动清除
  • RabbitMQ可以对消息或者整个队列设置ttl

已有的队列,加了ttl是无效的,需要新建的队列。

若依集成,在启动项目时,就会initQueue初始化队列。首先在rabbitMq的管理界面把以前有的队列(需要配置ttl的)先删除掉。

所以配置是需要放在消费者类

java 复制代码
@RabbitListener(
        queuesToDeclare = @Queue(
                value = "wj_place_msg",
                arguments = {
                        @Argument(name = "x-message-ttl", value = "10000", type = "java.lang.Integer"),
                }
        )
)

或者在配置类中增加

java 复制代码
 @Bean("placeOrderQueue")
    public Queue placeOrderQueue() {
        return QueueBuilder.durable(CloudConstant.MQ_JEECG_PLACE_ORDER)
                .ttl(10 * 1000)  // 10秒TTL
                .build();
    }

再重新启动

三、RabbitMQ 死信队列的实现

死信队列是 RabbitMQ 中用于处理无法正常消费的消息的特殊队列。当一个消息变成"死信"时,它会被重新发布到死信交换机,然后路由到死信队列。

  • 死信队列:DLX,dead letter exchange。
  • 当一个消息了以后,就会被发送到死信交换机DLX

队列绑定了DLX死信交换机,那么超时后,消息不会被抛弃,而是会进入到死信交换机,死信交换机绑定了其他队列(称之为死信队列),那么这个时候我们就可以处理那些被抛弃的消息了。

1.消息变成死信的三种情况

  1. 消息被拒绝(basic.reject或basic.nack消费者手动驳回消息)且 requeue=false
  2. 消息过期(TTL时间到): 队列设置:x-message-ttl=10000(10秒)
  3. 队列达到最大长度
java 复制代码
@RabbitListener(
        queuesToDeclare = @Queue(
                value = "wj_place_msg",
                arguments = {
                        @Argument(name = "x-message-ttl", value = "10000", type = "java.lang.Integer"),
                        @Argument(name = "x-dead-letter-exchange", value = DelayExchangeBuilder.DEAD_EXCHANGE),
                        @Argument(name = "x-dead-letter-routing-key", value = "dlx.routing.key")
                }
        )
)

其中死信队列可以不止一个。

死信队列本质上也是传统意义的队列,由业务队列绑定死信交换机及路由,然后分发到不同的死信队列其消费者进行死信处理。

2.若依集成设计

1)关于注解:

|--------|---------------------|----------|-----------------------------------------------------------------------------------------------------------|
| 编号 | 注解 | 配置位置 | 作用 |
| 1 | DeadLetterBinding | 业务消费者 | 绑定死信路由,交换因为只有一个,内置默认的。 需要注意的是,消费者配置同一个队列,死信路由也许一样,否则只有一个生效。见3)下说明 |
| 2 | DeadLetterQueueArgs | 死信消费者 | 死信队列的参数配置 |
| 3 | RabbitComponent | 所有消费者 | 配置值为首字母小写类名,用于加载消费者的 |
| 4 | RabbitQueueArgs | 业务消费者 | * 配置各种特殊队列的参数 * 延迟队列: 支持延迟消息功能 * 优先级队列: 支持消息优先级处理 * 懒加载队列: 优化内存使用 * 仲裁队列: 高可用队列类型(需要插件支持) |

2)初始加载:

java 复制代码
 @Bean
    public void initQueue() {
//        获取消费者
        Map<String, Object> beansWithRqbbitComponentMap = this.applicationContext.getBeansWithAnnotation(RabbitComponent.class);
        Class<? extends Object> clazz = null;
        for (Map.Entry<String, Object> entry : beansWithRqbbitComponentMap.entrySet()) {
            log.info("初始化队列............");
            //获取到实例对象的class信息
            clazz = entry.getValue().getClass();
            Method[] methods = clazz.getMethods();
            RabbitListener rabbitListener = clazz.getAnnotation(RabbitListener.class);
            if (ObjectUtil.isNotEmpty(rabbitListener)) {
                DeadLetterQueueArgs deadLetterQueueArgs = clazz.getAnnotation(DeadLetterQueueArgs.class);
                DeadLetterBinding rabbitBindDead = clazz.getAnnotation(DeadLetterBinding.class);
                RabbitQueueArgs rabbitQueueArgs = clazz.getAnnotation(RabbitQueueArgs.class);
                if (ObjectUtil.isNotEmpty(deadLetterQueueArgs)) {
//                创建死信队列
                    createDeadQueue(rabbitListener, deadLetterQueueArgs);
                } else {
//                创建业务队列
                    createBusinessQueue(rabbitListener, rabbitQueueArgs, rabbitBindDead);
                }
            }
            for (Method method : methods) {
                RabbitListener methodRabbitListener = method.getAnnotation(RabbitListener.class);
                if (ObjectUtil.isNotEmpty(methodRabbitListener)) {
                    DeadLetterQueueArgs methodDeadLetterQueueArgs = method.getAnnotation(DeadLetterQueueArgs.class);
                    DeadLetterBinding methodRabbitBindDead = method.getAnnotation(DeadLetterBinding.class);
                    RabbitQueueArgs methodRabbitQueueArgs = method.getAnnotation(RabbitQueueArgs.class);
                    if (ObjectUtil.isNotEmpty(methodDeadLetterQueueArgs)) {
                        //                创建死信队列
                        createDeadQueue(methodRabbitListener, methodDeadLetterQueueArgs);
                    } else {
                        //                创建业务队列
                        createBusinessQueue(methodRabbitListener, methodRabbitQueueArgs, methodRabbitBindDead);
                    }
                }
            }
        }

    }

3)配置需要注意的:

业务消费者

java 复制代码
@RabbitListener(queues = "wj_place_msg")
@RabbitComponent(value = "sendMsgConsumer1")
@DeadLetterBinding(deadRouter = "wj.dead.msg.send1")
@RabbitQueueArgs(
        ttl = 10 * 1000,      // 10秒测试TTL
        maxLength = 10000,         // 最多1万条消息
        maxPriority = 5            // 支持5个优先级
)
public class SendMsgConsumer1 extends BaseRabbiMqHandler<BaseMap>
java 复制代码
@RabbitListener(queues = "wj_place_msg")
@RabbitComponent(value = "sendMsgConsumer")
@DeadLetterBinding(deadRouter = "wj.dead.msg.send")
public class SendMsgConsumer extends BaseRabbiMqHandler<BaseMap>

我在两个消费者且同一队列wj_place_msg的情况,配置绑定死信队列路由用了不同的key。这是不行的,因为

复制代码
x-dead-letter-routing-key 只允许一个key。而且假如没有这个队列,必然读到其中一个消费者时,就会创建队列。读到下一个消费者时,已经不会再创建队列了。
复制代码
2026-01-29 10:30:25.489 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : === 收到死信消息 ===
2026-01-29 10:30:25.489 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息内容: {phone=13524533454}
2026-01-29 10:30:25.489 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息头: {spring_listener_return_correlation=9a6244be-fe17-4d1d-a7fc-31b1bb749b02, spring_returned_message_correlation=a131cb88-3642-4436-b0ad-607f18605da9, x-first-death-exchange=wj.direct.exchange, x-last-death-reason=rejected, x-death=[{reason=rejected, count=1, exchange=wj.direct.exchange, time=Thu Jan 29 10:30:19 CST 2026, routing-keys=[wj_place_msg], queue=wj_place_msg}], x-first-death-reason=rejected, x-first-death-queue=wj_place_msg, x-last-death-queue=wj_place_msg, x-last-death-exchange=wj.direct.exchange}
2026-01-29 10:30:25.733  WARN 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 死信原因: rejected, 来源队列: wj_place_msg, 死亡时间: Thu Jan 29 10:30:19 CST 2026
2026-01-29 10:30:25.733  WARN 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息被消费者明确拒绝
2026-01-29 10:30:26.620 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : === 收到死信消息 ===
2026-01-29 10:30:26.620 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息内容: {phone=13524533454}
2026-01-29 10:30:26.620 ERROR 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息头: {spring_listener_return_correlation=9a6244be-fe17-4d1d-a7fc-31b1bb749b02, spring_returned_message_correlation=41cb2208-70ea-4cab-9ca7-40db85b8c959, x-first-death-exchange=wj.direct.exchange, x-last-death-reason=rejected, x-death=[{reason=rejected, count=1, exchange=wj.direct.exchange, time=Thu Jan 29 10:30:19 CST 2026, routing-keys=[wj_place_msg], queue=wj_place_msg}], x-first-death-reason=rejected, x-first-death-queue=wj_place_msg, x-last-death-queue=wj_place_msg, x-last-death-exchange=wj.direct.exchange}
2026-01-29 10:30:26.951  WARN 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 死信原因: rejected, 来源队列: wj_place_msg, 死亡时间: Thu Jan 29 10:30:19 CST 2026
2026-01-29 10:30:26.952  WARN 93300 --- [ntContainer#1-1] c.w.c.m.l.MsgDeadLetterQueueListener     : 消息被消费者明确拒绝

4)如果死信消费者也出现问题呢?

死信消费者拒绝的终极处理架构

复制代码
1. 业务队列 → 普通死信队列
2. 普通死信队列 → 终极死信队列(永不拒绝)  
3. 终极死信队列 → 持久化存储
4. 持久化存储 → 人工处理 + 告警

实施步骤

  1. 配置终极死信队列(lazy模式 + 仲裁队列)

  2. 实现终极死信消费者(捕获所有异常)

  3. 建立多级持久化(DB → 文件 → 分布式存储)

  4. 配置智能告警(多渠道 + 重试机制)

  5. 实现监控自愈(健康检查 + 自动恢复)

  6. 提供管理界面(人工处理 + 数据分析)

这个方案适合信息可靠性要求很高的业务下。

相关推荐
xuzhiqiang07241 天前
Java进阶之路,Java程序员职业发展规划
java·开发语言
时艰.1 天前
订单系统历史数据归档方案
java
一只叫煤球的猫1 天前
ThreadForge v1.1.0 发布:让 Java 并发更接近 Go 的开发体验
java·后端·性能优化
014.1 天前
2025最新jenkins保姆级教程!!!
java·运维·spring boot·spring·jenkins
浣熊8881 天前
天机学堂虚拟机静态ip无法使用(重启后ip:192.168.150.101无法使用连接Mobaxterm数据库等等,或者无法使用修改之后的Hosts域名去访问nacos,jenkins)
java·微服务·虚拟机·天机学堂·重启之后静态ip用不了
心 -1 天前
java八股文IOC
java
I_LPL1 天前
day34 代码随想录算法训练营 动态规划专题2
java·算法·动态规划·hot100·求职面试
亓才孓1 天前
【MyBatis Exception】Public Key Retrieval is not allowed
java·数据库·spring boot·mybatis
J_liaty1 天前
Java设计模式全解析:23种模式的理论与实践指南
java·设计模式
Desirediscipline1 天前
cerr << 是C++中用于输出错误信息的标准用法
java·前端·c++·算法