1. 使用java.util.concurrent.DelayQueue
1.1定义
DelayQueue
是JDK提供的一个无界阻塞队列,内部基于优先队列实现。队列中的元素必须实现Delayed
接口,通过getDelay()
方法计算延迟时间。DelayQueue
是线程安全的,它通过内置的锁机制(ReentrantLock
)来保证多线程环境下的操作安全。主要的线程安全操作包括:-
入队操作(
put()
或offer()
):在添加新任务时,DelayQueue
会获取锁,将任务插入优先队列,并根据延迟时间调整堆结构。 -
出队操作(
take()
或poll()
):在获取任务时,DelayQueue
会检查堆顶任务是否到期。如果未到期,线程会进入等待状态,直到任务到期或被中断。 -
条件变量:
DelayQueue
使用Condition
来管理等待线程。当任务到期时,会唤醒等待的线程,使其能够继续执行。
-
1.2原理
源码如下
public class DelayQueue<E extends Delayed>
extends AbstractQueue<E>
implements BlockingQueue<E> {
/**
* 排它锁,用于保证线程安全
*/
private final transient ReentrantLock lock = new ReentrantLock();
/**
* 底层是基于PriorityQueue实现
*/
private final PriorityQueue<E> q = new PriorityQueue<E>();
/**
* 当前线程
*/
private Thread leader = null;
/**
* 条件队列
*/
private final Condition available = lock.newCondition();
}
DelayQueue
的底层是基于 PriorityQueue
实现的。PriorityQueue
是一个无界优先队列,它基于堆结构(最小堆)来维护元素的顺序。在 DelayQueue
中,队列会根据元素的延迟时间(getDelay()
方法返回的值)对元素进行排序,延迟时间越短的元素越先出队。
-
最小堆特性:优先队列的堆顶元素总是延迟时间最短的任务,即
getDelay()
返回值最小的元素。 -
动态调整:当任务的延迟时间到期后,队列会自动调整堆结构,确保下一个到期的任务位于堆顶。
1.3 例子
在这个例子中,DelayedTask
实现了Delayed
接口,DelayQueue
会根据getDelay()
方法的返回值对任务进行排序,并在延迟时间到期后返回。
为了使对象能够被 DelayQueue
管理,必须实现 Delayed
接口,该接口包含两个方法:
-
long getDelay(TimeUnit unit)
:返回当前时间与任务到期时间之间的剩余延迟时间(以指定的时间单位表示)。如果任务已经到期,则返回非正数(<= 0
)。 -
int compareTo(Delayed other)
:用于比较两个延迟任务的优先级。通常基于延迟时间进行比较,延迟时间越短的任务优先级越高。
import java.util.concurrent.*;
class DelayedTask implements Delayed {
private final long triggerTime;
private final String name;
public DelayedTask(String name, long delayTime, TimeUnit unit) {
this.name = name;
this.triggerTime = System.currentTimeMillis() + unit.toMillis(delayTime);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.triggerTime, o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
return "Task{" + name + "}";
}
}
public class DelayQueueExample {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedTask> queue = new DelayQueue<>();
queue.put(new DelayedTask("Task1", 2, TimeUnit.SECONDS));
queue.put(new DelayedTask("Task2", 1, TimeUnit.SECONDS));
while (!queue.isEmpty()) {
DelayedTask task = queue.take();
System.out.println("Executing: " + task);
}
}
}
2、使用Redis的ZSET
2.1定义
Redis
的有序集合(ZSET
)可以通过score
属性实现延迟队列。将元素的score
设置为过期时间戳,消费端轮询集合,比较score
与当前时间戳,从而实现延迟队列。
2.2原理
Redis 的有序集合(ZSET)是一种基于分数(score
)对元素进行排序的数据结构。每个元素都有一个唯一的分数,元素会根据分数从小到大自动排序。这一特性使得 ZSET 非常适合用于实现延迟队列,因为可以将任务的执行时间戳作为分数,从而按时间顺序排列任务。
(1)任务入队
当任务被添加到延迟队列时,会计算任务的执行时间戳(通常是当前时间戳加上延迟时间),并将任务内容和执行时间戳存入 ZSET。
-
计算执行时间戳:
execute_time = current_timestamp + delay
。 -
将任务存入 ZSET:使用
ZADD
命令将任务的执行时间戳作为分数,任务内容作为值存入 ZSET。
(2)任务出队
消费者会定期检查 ZSET 中分数小于等于当前时间戳的任务(即已经到期的任务),并执行这些任务。
-
获取到期任务:使用
ZRANGEBYSCORE
命令获取分数小于等于当前时间戳的任务。 -
执行任务:对每个到期任务执行相应的逻辑。
-
删除任务:任务执行完成后,使用
ZREM
命令从 ZSET 中删除该任务。
(3)定期检查
为了持续处理到期任务,消费者通常会启动一个定时任务,每隔一定时间(例如每秒)检查一次 ZSET。
-
轮询机制:消费者会不断循环,定期调用
ZRANGEBYSCORE
获取到期任务并处理。 -
优化:为了避免频繁轮询,可以结合 Redis 的发布/订阅机制或 Lua 脚本,减少不必要的检查
2.3例子
用Jedis实现
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
public class RedisDelayQueue {
private static final String DELAY_QUEUE = "delayQueue";
public static void main(String[] args) throws InterruptedException {
Jedis jedis = new Jedis("localhost", 6379);
jedis.del(DELAY_QUEUE); // 清空队列
// 添加延迟任务
jedis.zadd(DELAY_QUEUE, System.currentTimeMillis() + 5000, "Task1");
jedis.zadd(DELAY_QUEUE, System.currentTimeMillis() + 10000, "Task2");
// 消费端轮询
while (true) {
Tuple task = jedis.zrangeWithScores(DELAY_QUEUE, 0, 0).iterator().next();
long score = (long) task.getScore();
if (score <= System.currentTimeMillis()) {
System.out.println("Executing: " + task.getElement());
jedis.zrem(DELAY_QUEUE, task.getElement());
}
Thread.sleep(1000);
}
}
}
用redission实现
//生产者
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class DelayQueueProducer {
@Autowired
private RedissonClient redissonClient;
public void addTask(String task, long delay) {
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue("my_delayed_queue");
delayedQueue.offer(task, delay, TimeUnit.SECONDS);
System.out.println("Task added: " + task + ", with delay: " + delay + " seconds");
}
}
//消费者
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class DelayQueueConsumer {
@Autowired
private RedissonClient redissonClient;
@Scheduled(fixedDelay = 1000) // 每秒检查一次
public void processQueue() {
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue("my_delayed_queue");
String task = delayedQueue.poll(); // 获取到期任务
if (task != null) {
System.out.println("Processing task: " + task);
}
}
}
3、使用RabbitMQ
的延时队列
3.1定义
RabbitMQ
通过消息的TTL(Time To Live)和死信交换机(DLX)实现延时队列。
3.2原理
-
消息 TTL:为队列中的消息设置一个过期时间(TTL),当消息的 TTL 到期后,消息会被丢弃。
-
死信交换机(DLX):当消息因为 TTL 到期而被丢弃时,RabbitMQ 会将这些消息转发到一个指定的死信交换机(DLX)。DLX 再将消息路由到另一个队列,供消费者消费。
3.3例子
生产者
// 生产者
Channel channel = connection.createChannel();
channel.queueDeclare("delayQueue", true, false, false, null);
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.expiration("5000") // 设置消息TTL为5秒
.build();
channel.basicPublish("", "delayQueue", properties, "Hello, Delayed Message!".getBytes());
消费者
// 消费者
Channel channel = connection.createChannel();
channel.queueDeclare("delayQueue", true, false, false, null);
channel.basicConsume("delayQueue", true, (consumerTag, message) -> {
System.out.println("Received: " + new String(message.getBody()));
}, consumerTag -> {});