RabbitMQ死信延时队列阻塞问题

本文介绍了如何避免RabbitMQ死信队列中消息阻塞,通过为不同过期时间创建独立队列并配置死信处理,确保不同类型超时消息的及时执行。针对实习管理系统中的24小时和48小时过期需求,设计了四个定制队列及一个死信队列的解决方案。
部署运行你感兴趣的模型镜像

RabbitMQ死信延时队列阻塞问题

SHTL 博客:https://www.shtlls.ltd

问题
  1. 死信队列这种处理方式会出现一个问题:

    ​ 由于是一个队列,遵循先进先出原则,且每次检查只会判断第一个消息是否过期,不会每一个都判断,所以会出现长时间过期的消息会阻塞短时间过期的消息的情况,也就无法实现同一队列中多种超时时间间隔延时执行。

    ​ 这种方式也仅适用于过期时间一致的队列。

解决方法
可以根据不同的过期时间,设置不同的消息队列。
这个也仅针对过期时间类型仅为几种的情况,若间隔时间每一个都不一样,且无规律,则无法通过rabbitmq的死信队列实现,需要使用其他的方式。
  1. (针对实际项目<实习生管理系统>解决方法)目前有两种时间类型,一种是24小时,一种是48小时

    ​ 所以需要创建四个不同的延时队列,分别对应 22小时(79200000毫秒)、24小时(86400000毫秒)、46小时(165600000毫秒)、48小时(172800000毫秒)。

    ​ 将不同时间的消息存入不同的消息队列,将四个消息队列的过期消息都转发到一个死信队列,消费者只需要监听该死信队列即可。

代码简述
  1. 依赖

    <!-- spring-boot-starter-amqp -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-amqp</artifactId>
    	<version>2.6.1</version>
    </dependency>
    
  2. RabbitMqTtlConfig.java

    package com.macro.mall.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    public class RabiitMqTtlConfig {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
    
        @Bean("interviewTtlExchange")
        public DirectExchange ttlExchange() {
            return new DirectExchange("interview-ttl-exchange");
        }
    
        // 创建死信队列交换机 -direct类型
        @Bean("interviewDeadLetterExchange")
        public DirectExchange deadLetterExchange() {
            return new DirectExchange("interview-dead-letter-exchange");
        }
    
    
         @Bean("interviewTtlQueue22")
        public Queue ttlQueue22() {
            //设置在这个队列上的私信的去处->
            Map<String, Object> args = new HashMap<>(2);
            args.put("x-dead-letter-exchange", "interview-dead-letter-exchange");
            args.put("x-dead-letter-routing-key", "interview.dead.letter");
    
            //过期时间由队列统一设置
            //注意不是 x-expires,x-expires为队列存活时间,
            // x-message-ttl为队列内的消息存活时间
            //注意更改队列/交换机设置需要删除原有的
            return new Queue("interview-ttl-queue-22", true, false, false, args);
        }
    
        @Bean("interviewTtlQueue24")
        public Queue ttlQueue24() {
            //设置在这个队列上的私信的去处->
            Map<String, Object> args = new HashMap<>(2);
            args.put("x-dead-letter-exchange", "interview-dead-letter-exchange");
            args.put("x-dead-letter-routing-key", "interview.dead.letter");
            //过期时间由队列统一设置
            //注意不是 x-expires,x-expires为队列存活时间,
            // x-message-ttl为队列内的消息存活时间
            //注意更改队列/交换机设置需要删除原有的
            return new Queue("interview-ttl-queue-24", true, false, false, args);
        }
    
        @Bean("interviewTtlQueue46")
        public Queue ttlQueue46() {
            //设置在这个队列上的私信的去处->
            Map<String, Object> args = new HashMap<>(2);
            args.put("x-dead-letter-exchange", "interview-dead-letter-exchange");
            args.put("x-dead-letter-routing-key", "interview.dead.letter");
    
            //过期时间由队列统一设置
            //注意不是 x-expires,x-expires为队列存活时间,
            // x-message-ttl为队列内的消息存活时间
            //注意更改队列/交换机设置需要删除原有的
            return new Queue("interview-ttl-queue-46", true, false, false, args);
        }
    
        @Bean("interviewTtlQueue48")
        public Queue ttlQueue48() {
            //设置在这个队列上的私信的去处->
            Map<String, Object> args = new HashMap<>(2);
            args.put("x-dead-letter-exchange", "interview-dead-letter-exchange");
            args.put("x-dead-letter-routing-key", "interview.dead.letter");
    
            //过期时间由队列统一设置
            //注意不是 x-expires,x-expires为队列存活时间,
            // x-message-ttl为队列内的消息存活时间
            //注意更改队列/交换机设置需要删除原有的
            return new Queue("interview-ttl-queue-48", true, false, false, args);
        }
    
    
        // 绑定交换机与消息队列
        @Bean
        @DependsOn({"interviewTtlExchange", "interviewTtlQueue22"})
        public Binding ttlBinding22(@Qualifier("interviewTtlQueue22") Queue interviewTtlQueue22, Exchange interviewTtlExchange) {
            return BindingBuilder.bind(interviewTtlQueue22).to(interviewTtlExchange).with("interview.ttl.22").noargs();
        }
    
        @Bean
        @DependsOn({"interviewTtlExchange", "interviewTtlQueue24"})
        public Binding ttlBinding24(@Qualifier("interviewTtlQueue24") Queue interviewTtlQueue24, Exchange interviewTtlExchange) {
            return BindingBuilder.bind(interviewTtlQueue24).to(interviewTtlExchange).with("interview.ttl.24").noargs();
        }
    
        @Bean
        @DependsOn({"interviewTtlExchange", "interviewTtlQueue46"})
        public Binding ttlBinding46(@Qualifier("interviewTtlQueue46") Queue interviewTtlQueue46, Exchange interviewTtlExchange) {
            return BindingBuilder.bind(interviewTtlQueue46).to(interviewTtlExchange).with("interview.ttl.46").noargs();
        }
    
        @Bean
        @DependsOn({"interviewTtlExchange", "interviewTtlQueue48"})
        public Binding ttlBinding48(@Qualifier("interviewTtlQueue48") Queue interviewTtlQueue48, Exchange interviewTtlExchange) {
            return BindingBuilder.bind(interviewTtlQueue48).to(interviewTtlExchange).with("interview.ttl.48").noargs();
        }
    
        /**
         * 给22小时后过期消息队列发送消息
         */
        public void send22(String msg) {
            rabbitTemplate.convertAndSend("interview-ttl-exchange", "interview.ttl.22", msg, message -> {
                // 设置过期时间,过期之后从interview-ttl-queue死信转到interview-dead-letter-queue
                // 22小时后过期
                message.getMessageProperties().setExpiration("79200000");
                return message;
            });
        }
    
        /**
         * 给24小时后过期消息队列发送消息
         */
        public void send24(String msg) {
            rabbitTemplate.convertAndSend("interview-ttl-exchange", "interview.ttl.24", msg, message -> {
                // 设置过期时间,过期之后从interview-ttl-queue死信转到interview-dead-letter-queue
                // 24小时后过期
                message.getMessageProperties().setExpiration("86400000");
                return message;
            });
        }
    
        /**
         * 给46小时后过期消息队列发送消息
         */
        public void send46(String msg) {
            System.err.println("46\t" + msg);
            rabbitTemplate.convertAndSend("interview-ttl-exchange", "interview.ttl.46", msg, message -> {
                // 设置过期时间,过期之后从interview-ttl-queue死信转到interview-dead-letter-queue
                // 46小时后过期  165600000
                message.getMessageProperties().setExpiration("165600000");
                return message;
            });
        }
    
        /**
         * 给48小时后过期消息队列发送消息
         */
        public void send48(String msg) {
            System.err.println("48\t" + msg);
            rabbitTemplate.convertAndSend("interview-ttl-exchange", "interview.ttl.48", msg, message -> {
                // 设置过期时间,过期之后从interview-ttl-queue死信转到interview-dead-letter-queue
                // 48小时后过期   172800000
                message.getMessageProperties().setExpiration("172800000");
                return message;
            });
        }
    
    }
    
  3. RabbitMqSendUtils.java

    package com.macro.mall.utils;
    
    import com.alibaba.fastjson.JSON;
    import com.macro.mall.bo.InterviewSaveMqBO;
    import com.macro.mall.common.enums.ExpireEnum;
    import com.macro.mall.config.RabiitMqTtlConfig;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class RabbitMqSendUtils {
    
        @Autowired
        private RabiitMqTtlConfig rabiitMqTtlConfig;
    
        /**
         * 初筛过期定时提醒
         * @param bo
         */
        public void primaryScreeningSend(InterviewSaveMqBO bo) {
            bo.setExpire(ExpireEnum.ABOUT_TO_EXPIRE.getNum());
            rabiitMqTtlConfig.send22(JSON.toJSONString(bo));    // 设置22小时,过期前2小时提醒
            bo.setExpire(ExpireEnum.EXPIRED.getNum());
            rabiitMqTtlConfig.send24(JSON.toJSONString(bo));    // 设置24小时,过期提醒
        }
    
        /**
         * 一面、二面过期定时提醒
         * @param bo
         */
        public void firstSecondInterviewSend(InterviewSaveMqBO bo) {
            bo.setExpire(ExpireEnum.BIGIN_TO_INTERVIEW.getNum());
            System.out.println(JSON.toJSONString(bo));
            bo.setExpire(ExpireEnum.ABOUT_TO_EXPIRE.getNum());
            rabiitMqTtlConfig.send46(JSON.toJSONString(bo));    // 设置46小时,过期前2小时提醒
            bo.setExpire(ExpireEnum.EXPIRED.getNum());
            rabiitMqTtlConfig.send48(JSON.toJSONString(bo));    // 设置48小时,过期提醒
        }
    }
    
  4. DeadLetterListener.java

    package com.macro.mall.config;
    
    import com.alibaba.fastjson.JSON;
    import com.macro.mall.bo.InterviewSaveMqBO;
    import com.macro.mall.common.enums.InterviewEnum;
    import com.macro.mall.service.FirstInterviewService;
    import com.macro.mall.service.PrimaryScreeningService;
    import com.macro.mall.service.SecondInterviewService;
    import org.springframework.amqp.rabbit.annotation.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    /**
     * 监听RabbitMQ队列,处理定时邮件通知
     */
    @Component
    public class DeadLetterListener {
    
        @Autowired
        private PrimaryScreeningService primaryScreeningService;
        @Autowired
        private FirstInterviewService firstInterviewService;
        @Autowired
        private SecondInterviewService secondInterviewService;
    
        @RabbitHandler
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue("interview-dead-letter-queue"),
                exchange = @Exchange("interview-dead-letter-exchange"),
                key = "interview.dead.letter" ))
        public void onMessage(String msg) {
            InterviewSaveMqBO bo = JSON.parseObject(msg,InterviewSaveMqBO.class);
            // 以下可以对返回的消息进行处理了
            if (bo.getDesc().equals(InterviewEnum.PRIMARY_SCREENING.getDesc())) {
                primaryScreeningService.sendRemindMail(bo);
            } else if (bo.getDesc().equals(InterviewEnum.FIRST_INTERVIEW.getDesc())) {
                firstInterviewService.sendRemindMail(bo);
            } else if (bo.getDesc().equals(InterviewEnum.SECOND_INTERVIEW.getDesc())) {
                secondInterviewService.sendRemindMail(bo);
            }
        }
    
    }
    
图例
  1. Exchanges

    image-20211224140450451

  2. Queues

    image-20211224140513487

().equals(InterviewEnum.SECOND_INTERVIEW.getDesc())) {
secondInterviewService.sendRemindMail(bo);
}
}

  }
  ```
图例
  1. Exchanges

    [外链图片转存中…(img-FB98151d-1658905164202)]

  2. Queues

    [外链图片转存中…(img-SDSa9FJD-1658905164204)]

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

<think>首先,用户的问题是:“我想查找不使用RabbitMQ实现延时队列功能的方法 请问不使用RabbitMQ实现延时队列功能的解决方案”。用户明确表示不想使用RabbitMQ,而是寻找其他方法来实现延时队列延时队列是一种常见的需求,用于在特定时间后处理消息,比如订单超时未支付取消。用户引用了几个关于RabbitMQ实现延时队列的引用,但强调不使用RabbitMQ。 我需要基于系统级指令来构建回答: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题- 回答中引用的段落末尾自然地添加引用标识。 - 结构清晰,帮助用户逐步解决问题- 保证回答真实可靠。 用户提供的引用是关于RabbitMQ的,但用户不想使用RabbitMQ,所以我不能依赖这些引用。相反,我需要基于一般知识来回答。 常见的延时队列实现方法: 1. **Redis**: 使用有序集合(Sorted Set)来实现。消息的score设置为到期时间戳,消费者轮询或阻塞获取到期的消息。 2. **数据库**: 使用定时任务扫描数据库表,检查消息的到期时间。 3. **消息队列的其他实现**: 如Kafka、RocketMQ等支持延时消息。 4. **定时任务框架**: 如Quartz、Spring Scheduler等,通过调度任务来处理延时消息。 5. **内存队列**: 在应用中实现,使用优先级队列或时间轮算法。 逐步解决问题: 1. **介绍延时队列的概念**:解释什么是延时队列及其应用场景。 2. **列出替代方案**:详细描述不使用RabbitMQ的方法。 3. **优缺点分析**:帮助用户选择合适方案。 4. **代码示例**:如果适用,提供简单代码片段。 5. **生成相关问题**:在回答末尾添加相关问题。 生成相关问题:基于回答内容,提出用户可能感兴趣的后续问题。 回答结构: - **开头**:简要确认用户需求,并概述解决方案。 - **主体**:分步骤介绍不同方法。 - **结尾**:总结并添加相关问题。 确保LaTeX正确:如果提到数学表达式,如时间计算,使用$...$。例如,计算延时时间$t$。 引用标识:由于用户提供的引用是关于RabbitMQ的,不直接相关,但如果我引用一般知识,可能不需要外部引用。系统指令说“回答中引用的段落末尾自然地添加引用标识”,所以如果我从知识库引用,但这里没有特定知识库,我应避免不必要引用。或许可以忽略,除非必要。 用户上一次问题:用户说“上述引用仅供参考”,所以我应该基于一般知识。 最终回答框架: 1. 介绍延时队列。 2. 解决方案: - 使用Redis实现。 - 使用数据库实现。 - 使用其他消息队列(如Kafka)。 - 使用定时任务框架。 - 其他方法(如时间轮)。 3. 优缺点比较。 4. 代码示例(可选)。 5. 相关问题。 生成相关问题: 例如: 1. Redis实现延时队列的具体步骤是什么? 2. 如何保证延时队列的可靠性和性能? 3. 延时队列在分布式系统中如何实现?</think>### 不使用RabbitMQ实现延时队列功能的解决方案 延时队列是一种在指定时间后处理消息的机制,常用于订单超时取消、定时任务调度等场景[^5]。用户明确要求不使用RabbitMQ,以下是几种可靠的替代方案。我将逐步介绍每种方法的原理、实现步骤和优缺点,帮助您选择合适方案。所有方案均基于开源技术,易于集成到现有系统。 #### 1. **使用Redis实现延时队列** Redis的有序集合(Sorted Set)通过消息的score(到期时间戳)实现延时队列,消费者轮询或阻塞获取到期消息。时间复杂度为$O(\log N)$,高效且轻量。 - **实现步骤**: - 生产者:将消息ID和到期时间戳(如`ZADD delay_queue <timestamp> <message>`)添加到有序集合。 - 消费者:使用`ZRANGEBYSCORE`命令轮询score小于当前时间的消息,并移出集合处理。 - 可靠性:添加重试机制或持久化存储防止消息丢失。 - **优点**:简单高效,支持高并发;Redis单机QPS可达10万+。 - **缺点**:轮询可能引入延迟;需手动处理消息确认(如使用Lua脚本保证原子性)。 - **代码示例(Python)**: ```python import redis import time r = redis.Redis() # 生产者:添加延时消息(延时5秒) message = "order_cancel" delay_seconds = 5 r.zadd("delay_queue", {message: time.time() + delay_seconds}) # 消费者轮询 while True: now = time.time() messages = r.zrangebyscore("delay_queue", 0, now) for msg in messages: print(f"Processing: {msg.decode()}") r.zrem("delay_queue", msg) # 移除已处理消息 time.sleep(0.1) # 避免CPU占用过高 ``` #### 2. **使用数据库定时扫描实现** 通过数据库表存储消息和到期时间,结合定时任务(如Cron或Spring Scheduler)扫描并处理到期消息。适用于数据量小的场景。 - **实现步骤**: - 建表:创建表存储消息ID、内容、到期时间戳$t_{\text{expire}}$和状态。 - 生产者:插入消息时设置$t_{\text{expire}} = \text{current_time} + \text{delay}$。 - 消费者:定时任务(如每10秒)查询`WHERE status='pending' AND t_expire <= NOW()`,处理并更新状态。 - 优化:添加索引加速查询;使用分布式锁避免重复处理。 - **优点**:无需额外中间件,利用现有数据库;易于实现事务。 - **缺点**:扫描频率影响实时性;高并发下数据库压力大,不适合大规模场景。 - **适用场景**:中小型系统,如订单超时管理[^5]。 #### 3. **使用其他消息队列(如Apache Kafka或RocketMQ)** Kafka和RocketMQ原生支持延时消息。Kafka通过时间轮算法实现,RocketMQ提供多级延时(如1s/5s/10s)。 - **实现步骤**: - Kafka:生产者发送消息时设置`delivery.timeout.ms`参数,消费者订阅主题处理。 - RocketMQ:发送消息指定`delayTimeLevel`(延时级别),Broker自动投递到死信队列。 - 可靠性:消息持久化保证不丢失;支持集群部署。 - **优点**:高吞吐量(Kafka可达百万级QPS);内置延时机制,无需额外开发。 - **缺点**:部署复杂;Kafka延时精度较低(默认1秒级)。 - **推荐场景**:大数据或分布式系统,如物流状态更新。 #### 4. **使用定时任务框架(如Quartz或Spring Scheduler)** 在应用层实现,通过内存队列(如优先级队列)或时间轮算法调度延时任务。 - **实现步骤**: - 时间轮算法:将时间划分为槽,每个槽存储到期消息,指针移动触发处理。 - 框架集成:Spring Scheduler配置`@Scheduled`注解扫描任务队列。 - 示例:使用Java的`DelayQueue`(基于优先级堆),元素实现`Delayed`接口。 - **优点**:零外部依赖;延时精度高(毫秒级)。 - **缺点**:应用重启可能导致消息丢失;不适合分布式环境(需结合ZooKeeper协调)。 - **代码片段(Java)**: ```java import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayQueueExample { static class DelayedMessage implements Delayed { String message; long expireTime; public DelayedMessage(String msg, long delayMs) { this.message = msg; this.expireTime = System.currentTimeMillis() + delayMs; } @Override public int compareTo(Delayed o) { return Long.compare(this.expireTime, ((DelayedMessage)o).expireTime); } } public static void main(String[] args) throws InterruptedException { DelayQueue<DelayedMessage> queue = new DelayQueue<>(); queue.put(new DelayedMessage("order_cancel", 5000)); // 延时5秒 while (true) { DelayedMessage msg = queue.take(); System.out.println("Processing: " + msg.message); } } } ``` #### 方案选择建议 - **高性能需求**:优先选Redis或Kafka,QPS高且扩展性强。 - **简单可靠**:数据库方案易实现,但注意表索引优化。 - **无中间件环境**:定时任务框架最快,但需处理持久化问题。 所有方案需考虑消息去重和错误重试,常用方法如幂等设计或日志追踪[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值