Redis延迟队列原理及实例

本文详细介绍了如何使用Redis的SortedSet实现延迟队列,包括其原理、数据结构优势、Jedis、Redisson和RedisTemplate三种实现方法,并推荐使用Redisson以提高资源效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Redis延迟队列的原理

在Redis延迟队列中,我们使用有序集合(Sorted Set)来存储消息,其中,消息的到期时间作为有序集合的分数,消息内容作为有序集合的成员。例如,我们向Redis中添加一个到期时间为2023年12月25日的消息,可以使用以下命令:

ZADD delay_queue 1640390400 "message content"

在这个命令中,delay_queue是有序集合的名称,1640390400是消息的到期时间(即2023年12月25日的时间戳),"message content"是消息的内容。

当我们需要消费这个延迟消息时,需要定期轮询有序集合,找到所有到期的消息进行消费。可以使用以下命令:

ZRANGEBYSCORE delay_queue 0 CURRENT_TIMESTAMP

其中,CURRENT_TIMESTAMP为当前时间戳,该命令的作用是获取当前时间之前的所有消息。然后,我们可以遍历返回结果,依次进行消息的处理。

对于已经处理完成的消息,可以选择从有序集合中删除,也可以将其留在有序集合中,等待下一次轮询。如果希望删除消息,可以使用以下命令:

ZREM delay_queue "message content"

通过这种方式,Redis延迟队列可以实现消息的延迟投递和消费。因为消息按照到期时间排序,所以可以保证消息的有序性。此外,由于Redis本身是一种高性能内存数据库,所以延迟队列的处理效率也非常高。

二、数据结构说明

2.1、数据结构说明

Sorted Set(有序集合)在Redis中也被称为 ZSet(有序集合)。在Redis中,ZSet提供了有序的、唯一的成员(member)和对应的分数(score)之间的映射关系。这种数据结构非常适合用来实现延迟队列,因为可以根据分数进行范围查询,从而轻松地获取到期的消息。

2.2、为什么Sorted Set适合做延迟队列

Sorted Set(有序集合)适合用于实现延迟队列的主要原因有以下几点:

  1. 有序性:Sorted Set中的成员按照分数进行排序,这使得我们可以方便地根据到期时间进行范围查询。通过设置分数为消息的到期时间,可以轻松地获取到期的消息,而无需遍历整个队列。

  2. 唯一性:Sorted Set中的成员是唯一的,这意味着我们可以确保消息不会重复。这对于一些需要确保消息处理幂等性的场景非常重要。

  3. 高效性:Redis是一种高性能的内存数据库,Sorted Set的操作效率很高。在使用Sorted Set实现延迟队列时,添加、删除和范围查询等操作都可以在O(logN)的时间复杂度内完成,具有良好的性能表现。

  4. 支持多种操作:除了基本的添加、删除和范围查询,Sorted Set还支持其他一些有用的操作,如获取成员的分数、修改成员的分数、按照分数范围删除成员等。这些操作可以为延迟队列的管理提供更多的灵活性和功能。

综上所述,Sorted Set具备有序性、唯一性、高效性和丰富的操作特性,使其成为实现延迟队列的理想选择。在Redis中利用Sorted Set来实现延迟队列,可以简单高效地处理延迟任务。

2.3、Sorted Set内部结构

Sorted Set 能够按照分数进行排序的原因在于它内部使用了一种数据结构——跳跃表(Skip List)。跳跃表是一种有序链表的数据结构,它通过在不同层级上建立索引的方式,提高了基本有序链表的查找效率。在 Redis 中,Sorted Set 的实现就是基于跳跃表。

通过跳跃表,Sorted Set 在插入新成员时可以保持成员的有序性。当需要按照分数进行排序时,Redis 内部会利用跳跃表的特性,快速地定位到对应分数的成员,从而实现按照分数进行排序的功能。

跳跃表的查询时间复杂度为 O(logN),这意味着无论集合中有多少成员,按照分数进行范围查询的效率都很高。这也是为什么 Sorted Set 能够高效地支持按照分数进行排序的重要原因之一。

总的来说,跳跃表作为 Sorted Set 内部的数据结构,通过其高效的有序性和索引特性,使得 Sorted Set 能够轻松地按照分数进行排序。

三、三种实现方式

3.1、 Jedis实现方式

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

import java.util.Set;

public class RedisDelayQueue {
    private static final String DELAY_QUEUE_KEY = "delay_queue";

    public static void main(String[] args) {
        // 连接 Redis
        Jedis jedis = new Jedis("localhost");

        // 添加延迟消息
        addMessageToDelayQueue(jedis, "message1", 5000);  // 延迟 5 秒发送
        addMessageToDelayQueue(jedis, "message2", 10000); // 延迟 10 秒发送

        // 处理延迟消息
        processDelayQueue(jedis);

        // 断开连接
        jedis.close();
    }

    // 将消息添加到延迟队列
    private static void addMessageToDelayQueue(Jedis jedis, String message, long delayMillis) {
        long currentTime = System.currentTimeMillis();
        long delayTime = currentTime + delayMillis;

        jedis.zadd(DELAY_QUEUE_KEY, delayTime, message);
        System.out.println("Message added to delay queue: " + message);
    }

    // 处理延迟队列中的消息
    private static void processDelayQueue(Jedis jedis) {
        while (true) {
            long currentTime = System.currentTimeMillis();

            // 获取当前时间之前的第一个消息及其分数
            Set<Tuple> messages = jedis.zrangeByScoreWithScores(DELAY_QUEUE_KEY, 0, currentTime, 0, 1);

        if (messages.isEmpty()) {
            // 延迟队列为空,暂停一段时间再继续轮询
            try {
                Thread.sleep(1000); // 暂停 1 秒钟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            continue;
        }

            // 处理消息,这里简单打印消息内容
            for (Tuple tuple : messages) {
                String message = tuple.getElement();
                double score = tuple.getScore();

                System.out.println("Processing message: " + message);
                System.out.println("Scheduled time: " + score);

                // 从延迟队列中移除已处理的消息
                jedis.zrem(DELAY_QUEUE_KEY, message);
            }
        }
    }
}

3.2、Redisson实现(推荐

Redisson 是一个基于 Redis 的 Java 客户端,Redisson 提供了 RDelayedQueue 接口和 RQueue, RBlockingDeque 接口来实现延迟队列。
原理如下:

  • 首先,你需要创建一个基本的队列,然后将它包装在一个延迟队列中。将延迟任务添加到延迟队列中,指定任务的内容和延迟时间(以毫秒为单位)。
  • 延迟队列会自动处理过期的任务并将它们移动到基本队列中。你可以从基本队列中获取任务并进行处理。

过程有点像RabbitMq, 延迟队列 + TTL + 死信队列,用两个队列结合使用。

下面是使用 Redisson 实现延迟队列的示例代码:

  1. 创建两个队列,并关联
@Bean
public RDelayedQueue delayedQueue(RedissonClient redissonClient) {
 	// 创建阻塞队列
 	RBlockingDeque<String> queue = redissonClient.getBlockingDeque("redisson_delay_Queue");// 创建延迟队列并关联到基本队列RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);return delayedQueue;
}

  1. 添加延迟任务到延迟队列中
// 添加延迟任务
delayedQueue.offer("Task1", 5000); // 5秒延迟
delayedQueue.offer("Task2", 10000); // 10秒延迟
  1. 处理延迟任务
    延迟队列会自动处理过期的任务并将它们移动到阻塞队列中。你可以从阻塞队列中获取任务并进行处理。
while (true){
	// 获取并移除队首元素,如果队列为空,则阻塞等待
    String take = queue.take();
    log.info("当前时间:{},值:{}", LocalDateTime.now(),take);
}

为什么要使用阻塞队列 RBlockingDeque,而不使用普通队列RQueue,普通队列在获取队列元素时没有阻塞的功能,在while(true)中一直循环,导致cpu空转,造成资源浪费。阻塞队列的话,队列没有元素会阻塞着。

  1. 单测实例
@Test
public void redisDelayedQueueTest() {
   RBlockingDeque<String> test = redissonClient.getBlockingDeque("test");
   RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(test);

        delayedQueue.offer("1",20, TimeUnit.SECONDS);
        delayedQueue.offer("2",10, TimeUnit.SECONDS);
        log.info("当前时间:{}", LocalDateTime.now());
        while (true){
            String take = test.take();
            log.info("当前时间:{},值:{}", LocalDateTime.now(),take);
        }
}

3.3、RedisTemplate 实现

import com.alibaba.fastjson.JSON;
import com.jingdianjichi.redis.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Description: 群发任务延时service
 * @DateTime: 2023/12/8 23:24
 */
@Service
@Slf4j
public class MassMailTaskService {

    @Resource
    private RedisUtil redisUtil;

    private static final String MASS_MAIL_TASK_KEY = "massMailTask";

    public void pushMassMailTaskQueue(MassMailTask massMailTask) {
        Date startTime = massMailTask.getStartTime();
        if (startTime == null) {
            return;
        }
        if (startTime.compareTo(new Date()) <= 0) {
            return;
        }
        log.info("定时群发任务加入延时队列,massMailTask:{}", JSON.toJSON(massMailTask));
        redisUtil.zAdd(MASS_MAIL_TASK_KEY, massMailTask.getTaskId().toString(), startTime.getTime());
    }

    public Set<Long> poolMassMailTaskQueue() {
        Set<String> taskIdSet = redisUtil.rangeByScore(MASS_MAIL_TASK_KEY, 0, System.currentTimeMillis());
        log.info("获取延迟群发任务,taskIdSet:{}", JSON.toJSON(taskIdSet));
        if (CollectionUtils.isEmpty(taskIdSet)) {
            return Collections.emptySet();
        }
        redisUtil.removeZsetList(MASS_MAIL_TASK_KEY, taskIdSet);
        return taskIdSet.stream().map(n -> Long.parseLong(n)).collect(Collectors.toSet());
    }


}

@Component
@Slf4j
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    public Boolean zAdd(String key, String value, Long score) {
        return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
    }

    public void removeZsetList(String key, Set<String> value) {
        value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
    }

    public Set<String> rangeByScore(String key, long start, long end) {
        return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
    }



}

四、总结

个人建议使用Redisson的方式。因为队列中没有元素时,会阻塞。从而避免了while(true) 一直循环,空转cpu, 浪费资源~ 。其他两种方式在while(true)中获取不到元素会一直死循环,要手动设置线程休眠时间~

<think>好的,用户想了解如何使用Redission实现延迟队列的示例教程。我需要先回忆一下Redission的相关知识。记得Redission是一个基于RedisJava客户端,提供了很多分布式服务,其中就包括延迟队列的功能。延迟队列通常用于需要延迟处理任务的场景,比如订单超时关闭、定时提醒等。 用户提供的引用中提到了两种方案,其中第二种是Redission实现延迟队列。引用[1]提到Redisson实现延迟队列是比较靠谱的方案,引用[2]给出了引入Redission的Maven依赖。所以,我需要结合这些信息来构建一个示例教程。 首先,我应该从配置Redission客户端开始,因为使用前需要连接到Redis服务器。配置部分可能需要创建一个Config对象,指定Redis服务器的地址和端口。然后初始化RedissonClient实例。 接下来,需要介绍如何创建延迟队列Redission提供了RDelayedQueue接口,它需要绑定一个目标队列和一个延迟时间。用户需要先创建目标队列,比如使用RBlockingQueue,然后再创建RDelayedQueue,将目标队列和延迟时间关联起来。 然后,是生产者和消费者的实现。生产者使用延迟队列的offer方法添加元素,并指定延迟时间。消费者则从目标队列中获取元素,这里可能需要使用take方法,该方法会阻塞直到有元素可用。 还需要注意线程管理,尤其是在消费者端,可能需要启动一个单独的线程来持续监听队列。同时,异常处理和资源释放也是关键点,比如在程序结束时关闭Redission客户端,避免资源泄漏。 用户可能还关心实际的应用场景和注意事项,比如消息持久化、可靠性、性能等。这些应该在注意事项部分提到,确保用户了解如何在实际项目中正确使用。 最后,生成相关问题,帮助用户进一步学习,比如优化方法、异常处理、与其他方案的比较等。这些问题需要围绕Redission延迟队列的实现细节和应用展开。 现在,需要检查是否有遗漏的步骤,确保示例代码正确,并且符合用户的需求。比如,是否正确使用RDelayedQueue和RBlockingQueue的组合,是否正确处理了线程和资源管理。同时,引用部分是否正确标注,引用了用户提供的参考内容。 总结下来,回答的结构应该是先介绍Redission延迟队列的基本原理,然后分步骤讲解配置、队列创建、生产者和消费者的实现,最后给出注意事项和相关问题。确保代码示例清晰,并且有详细的注释说明每一步的作用。</think>### Redission实现延迟队列教程 Redission通过`RDelayedQueue`接口实现延迟队列功能,其核心机制是将元素暂存于中间队列,Redis自动管理到期时间后转移至目标队列[^1]。 #### 一、环境配置 1. 添加Maven依赖: ```xml <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.16.6</version> </dependency> ``` [^2] 2. 创建Redisson客户端: ```java Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); ``` #### 二、队列创建 ```java // 目标队列(实际消费队列) RBlockingQueue<String> destinationQueue = redisson.getBlockingQueue("destinationQueue"); // 延迟队列(绑定目标队列,设置5秒延迟) RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(destinationQueue); delayedQueue.offer("task1", 5, TimeUnit.SECONDS); // 5秒后进入目标队列 ``` #### 三、生产者实现 ```java public class Producer { public static void addDelayedTask(RDelayedQueue<String> queue, String task, long delay) { queue.offer(task, delay, TimeUnit.SECONDS); System.out.println("添加延迟任务:" + task); } } ``` #### 四、消费者实现 ```java public class Consumer implements Runnable { private RBlockingQueue<String> queue; public Consumer(RBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { String task = queue.take(); // 阻塞获取元素 System.out.println("处理任务:" + task); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } ``` #### 五、启动服务 ```java // 启动消费者线程 new Thread(new Consumer(destinationQueue)).start(); // 添加测试任务 Producer.addDelayedTask(delayedQueue, "支付超时订单", 30); Producer.addDelayedTask(delayedQueue, "库存释放任务", 120); ``` #### 注意事项 1. 消息持久化:Redis默认开启RDB持久化,但建议配置AOF确保可靠性 2. 异常处理:需捕获`RedisResponseTimeoutException`等网络异常 3. 资源释放:程序退出时调用`delayedQueue.destroy()`和`redisson.shutdown()`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值