JAVA面试-延迟队列的实现方式

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 -> {});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值