延迟队列的实现

137 阅读2分钟

1. 使用DelayQueue实现延迟队列

DelayQueue是JDK中提供的延迟队列的实现方式,它继承了AbstractQueue抽象类,并实现了BlockingQueue接口。

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    // 略
}

可以看到,延迟队列使用了ReentrantLock保证线程安全,其底层实际上是优先队列PriorityQueue,队列内的元素是继承了Delayed接口的对象。

编写一个类实现Delayed接口。其中使用泛型来存储对象,使用long来表示对象可获取的时间。

public class DelayObject<T> implements Delayed {
    private final T obj;
    private final long availableTime;

    public DelayObject(T obj, long delayTime) {
        this.obj = obj;
        // 由当前时间 + 延期时间
        this.availableTime = System.currentTimeMillis() + delayTime;
    }

    /**
     * 获取对象
     */
    public T getObj() {
        return obj;
    }

    /**
     * 获取delay
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(availableTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 排序
     */
    @Override
    public int compareTo(Delayed o) {
        long l = getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
        return l == 0 ? 0 : l > 0 ? 1 : -1;
    }
}

测试延迟队列:

public static void main(String[] args) {
    // 创建延迟队列
    DelayQueue<DelayObject<User>> queue = new DelayQueue<>();

    // 填充队列
    fillQueue(queue);

    // 线程池执行任务
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(() -> {
        while (true) {
            try {
                DelayObject<User> delayObject = queue.take();
                System.out.printf("Take user from delayed queue %s:%s at %s%n", delayObject.getObj().getId(),
                        delayObject.getObj().getName(),
                        LocalDateTime.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

private static void fillQueue(DelayQueue<DelayObject<User>> queue) {
    List<User> users = Arrays.asList(new User("1", "Alice"),
            new User("2", "Bob"),
            new User("3", "Celina"));

    // 初始延迟
    int delay = 5000;
    for (User user : users) {
        // 放入延迟队列
        System.out.printf("Begin to offer user to delayed queue %s:%s at %s%n", user.getId(),
                user.getName(), LocalDateTime.now());
        queue.offer(new DelayObject<>(user, delay));
        delay += 5000;
    }
}

2. 使用Redisson实现延迟队列

Redisson是Redis官方推荐的Java客户端,除了用来实现分布式锁之外,还可以用来实现延迟队列。

public void addDelay() {
    // 根据队列名称获取阻塞队列
    RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(DELAY_QUEUE);
    // 根据阻塞队列获取延迟队列
    RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);

    List<User> users = Arrays.asList(new User("1", "Alice"),
            new User("2", "Bob"),
            new User("3", "Celina"));
    int delay = 5;
    
    // 插入延迟队列
    for (User user : users) {
        log.info("Begin to offer user to delayed queue {}:{} at {}", user.getId(), user.getName(),
                LocalDateTime.now());
        delayedQueue.offer(user, delay, TimeUnit.SECONDS);
        delay += 5;
    }
}

可以使用同样的方法从延迟队列中获取对象。

@Slf4j
@Component
public class AppStartupRunner implements ApplicationRunner {
    private static final String DELAY_QUEUE = "delay_queue";

    @Resource
    private RedissonClient redissonClient;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            RBlockingDeque<User> blockingDeque = redissonClient.getBlockingDeque(DELAY_QUEUE);
            // 必须调用getDelayedQueue方法,否则获取不到已过期数据
            redissonClient.getDelayedQueue(blockingDeque);
            // 循环获取
            while (true) {
                try {
                    User user = blockingDeque.take();
                    log.info("Take user from delayed queue {}:{} at {}", user.getId(), user.getName(),
                            LocalDateTime.now());
                } catch (InterruptedException e) {
                    log.error("Failed to take from blocking deque caused by {}", e.getMessage());
                }
            }
        });
    }
}