一、延迟任务
延迟任务(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:如果任务数据特别大,为了防止阻塞,只需要把未来几分钟要执行的数
据存入缓存即可,是一种优化的形式