【Java】基于Redis的Java延迟任务实现


一、延迟任务

延迟任务(Delayed Task)是一种任务调度策略,用于将任务推迟到指定的时间之后再执行。这在各种应用场景中都有广泛应用,如文章定时发布,电商订单锁定尚未支付倒计时等场景。


二、延迟任务常见实现方法

1.DelayQueue

JDK自带DelayQueue 是一个支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
在这里插入图片描述
使用DelayQueue作为延迟任务,如果程序挂掉之后,任务都是放在内存,消息会丢失,如何保证数据不丢失

2.RabbitMQ

TTL:Time To Live(消息存活时间)
死信队列:Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以重新发送另一个交换机(死信交换机)
在这里插入图片描述
示例代码:

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

public class DelayedTaskExample {

    private static final String EXCHANGE_NAME = "delayed_exchange";
    private static final String QUEUE_NAME = "delayed_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try (Connection connection = factory.newConnection(); 
             Channel channel = connection.createChannel()) {

            // 配置延迟交换机参数
            Map<String, Object> argsMap = new HashMap<>();
            argsMap.put("x-delayed-type", "direct");

            // 声明延迟交换机
            channel.exchangeDeclare(EXCHANGE_NAME, "x-delayed-message", true, false, argsMap);

            // 声明队列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            // 绑定队列到交换机
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

            // 发送延迟消息
            String message = "Task executed after delay!";
            Map<String, Object> headers = new HashMap<>();
            headers.put("x-delay", 5000);  // 延迟 5000 毫秒 (5 秒)

            AMQP.BasicProperties.Builder propsBuilder = new AMQP.BasicProperties.Builder();
            propsBuilder.headers(headers);

            channel.basicPublish(EXCHANGE_NAME, "", propsBuilder.build(), message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");

            // 消费消息
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String receivedMessage = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + receivedMessage + "'");
            };
            channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
        }
    }
}

3.Redis

在Java中使用Redis实现延迟任务可以通过Redis的有序集合(Sorted Set)来实现。这种方式利用了有序集合中的成员按照分数(score)排序的特性,我们可以将任务存储在有序集合中,并使用分数表示任务的执行时间戳。当需要执行任务时,只需取出分数小于或等于当前时间戳的任务即可。


三、Redis实现延迟任务

1.原理

首先将任务数据添加到数据库里,再由数据库导入到Redis,导入之前会先进行判断任务执行时间是否小于等于当前时间,如果是,则代表当前任务立即执行,将其放入到Redis的消费队列中消费任务。如果否则判断执行时间是否小于等于预设时间,如果是,则将其放入Redis的zset有序集合中等待执行,在Redis中每分钟定时刷新,如果延迟任务需要执行的话,就会从zset进入到list里执行,Redis中只有list才能消费任务。最后数据库需要定时同步延迟任务,因为Redis是单线程,存储不了大数据。
在这里插入图片描述

2.简单实现

生产者代码(添加延迟任务)

import redis.clients.jedis.Jedis;

public class DelayedTaskProducer {

    private static final String DELAYED_TASKS_KEY = "delayed_tasks";

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

        // 任务的内容
        String task = "Task to be executed after delay";

        // 当前时间加上延迟时间(例如 5 秒)作为分数
        long delayInSeconds = 5;
        long executionTime = System.currentTimeMillis() / 1000 + delayInSeconds;

        // 将任务添加到有序集合中
        jedis.zadd(DELAYED_TASKS_KEY, executionTime, task);
        System.out.println("Task scheduled to be executed after " + delayInSeconds + " seconds");

        jedis.close();
    }
}

消费者代码(执行延迟任务)

import redis.clients.jedis.Jedis;

import java.util.Set;

public class DelayedTaskConsumer {

    private static final String DELAYED_TASKS_KEY = "delayed_tasks";

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

        while (true) {
            // 获取当前时间戳
            long currentTime = System.currentTimeMillis / 1000;

            // 获取分数小于等于当前时间戳的任务
            Set<String> tasks = jedis.zrangeByScore(DELAYED_TASKS_KEY, 0, currentTime);

            // 执行任务
            for (String task : tasks) {
                System.out.println("Executing task: " + task);
                // 从有序集合中移除已执行的任务
                jedis.zrem(DELAYED_TASKS_KEY, task);
            }

            try {
                // 每隔一秒检查一次
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.问题

(1).Q:为什么任务需要存储在数据库中?
A: 延迟任务是一个通用的服务,任何有延迟需求的任务都可以调用该服务
,内存数据库的存储是有限的,需要考虑数据持久化的问题,存储数据
库中是一种数据安全的考虑。

(2).Q:为什么使用redis中的两种数据类型,list和zset?
A:原因一:list存储立即执行的任务,zset存储未来的数据
原因二:任务量过大以后,zset的性能会下降
时间复杂度比较起来list命令LPUSH:时间复杂度:O(1),zset命令zadd:时间复杂度:O(M*log(n)

(3).Q:在添加zset数据的时候,为什么需要预加载?
A:如果任务数据特别大,为了防止阻塞,只需要把未来几分钟要执行的数
据存入缓存即可,是一种优化的形式

Java中利用Redis实现定时任务通常通过结合Spring框架和Jedis客户端来完成,因为Spring支持时间和延迟操作,而Redis提供了一个方便的数据结构`Sorted Set`或`List`来存储定时任务。以下是基本步骤: 1. **配置Spring Redis连接**: 首先,在Spring的配置文件中配置Redis连接,比如`application.properties`或`application.yml`: ```properties spring.redis.host=your_redis_host spring.redis.port=your_redis_port ``` 2. **创建定时任务存储结构**: 使用`SortedSet`(按照时间戳排序)或`List`(先进先出)存储定时任务,每个任务包含一个键和一个过期时间(通常是Unix时间戳加上延迟)。 ```java @Data class Task { private String key; private long timestamp; // 过期时间 } ``` 3. **添加定时任务Redis**: 在需要执行定时任务的地方,创建一个Task实例并将其添加到Redis: ```java Jedis jedis = JedisConnectionFactory.getConnection(); jedis.zadd("tasks", System.currentTimeMillis() + delay, task.getKey()); ``` 4. **定时检查任务执行**: 使用Spring的定时任务功能,如`@Scheduled`注解,定期扫描Redis中的任务集合,找到即将过期的任务,并执行对应的业务逻辑: ```java @Scheduled(cron = "0/5 * * * *") // 每5秒检查一次 public void executeTasks() { Jedis jedis = JedisConnectionFactory.getConnection(); ZSet range = jedis.zrangeByScore("tasks", 0, System.currentTimeMillis()); for (String key : range) { Long timestamp = jedis.zscore("tasks", key); if (timestamp > System.currentTimeMillis()) { // 执行对应的任务逻辑,这里仅示例,实际应替换为对key的业务处理 jedis.del(key); } else { // 完成任务后删除 jedis.del(key); // 调用业务逻辑 handleTask(key); } } } private void handleTask(String key) { // 根据key获取具体的业务逻辑并执行 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值