java延时触发

33 阅读4分钟

1.定时任务

优点:实现容易,成本低,基本不依赖其他组件。

缺点:

时间可能不够精确。由于定时任务扫描的间隔是固定的,所以可能造成一些订单已经过

期了一段时间才被扫描到,订单关闭的时间比正常时间晚一些。

增加了数据库的压力。随着订单的数量越来越多,扫描的成本也会越来越大,执行时间

也会被拉长,可能导致某些应该被关闭的订单迟迟没有被关闭。

总结采用定时任务的方案比较适合对时间要求不是很敏感,并且数据量不太多的业务

场景。

2 JDK 延迟队列 DelayQueue

用户的订单生成以后,设置过期时间比如 30 分钟,放入定义好的 DelayQueue,然

后创建一个线程,在线程中通过 while(true)不断的从 DelayQueue 中获取过期的数

据。

优点:不依赖任何第三方组件,连数据库也不需要了,实现起来也方便。

缺点:

因为 DelayQueue 是一个无界队列,如果放入的订单过多,会造成 JVM OOM。

DelayQueue 基于 JVM 内存,如果 JVM 重启了,那所有数据就丢失了。

总结DelayQueue 适用于数据量较小,且丢失也不影响主业务的场景,比如内部系 统的一些非重要通知,就算丢失,也不会有太大影响。

3 redis 过期监听

redis 是一个高性能的 KV 数据库,除了用作缓存以外,其实还提供了过期监听的功能。

在 redis.conf 中,配置 notify-keyspace-events Ex 即可开启此功能。

然后在代码中继承 KeyspaceEventMessageListener,实现 onMessage 就可以监听 过期的数据量。

其本质也是注册一个 listener,利用 redis 的发布订 阅,当 key 过期时,发布过期消息(key)到 Channel :keyevent@*:expired 中。

在实际的业务中,我们可以将订单的过期时间设置比如 30 分钟,然后放入到 redis。 30 分钟之后,就可以消费这个 key,然后做一些业务上的后置动作,比如检查用户是 否支付。

优点: 由于 redis 的高性能,所以我们在设置 key,或者消费 key 时,速度上是可 以保证的。

缺点:由于 redis 的 key 过期策略原因,当一个 key 过期时,redis 无法保证立刻 将其删除,自然我们的监听事件也无法第一时间消费到这个 key,所以会存在一定的

延迟。另外,在 redis5.0 之前,订阅发布中的消息并没有被持久化,自然也没有所谓的确认机制。所以一旦消费消息的过程中我们的客户端发生了宕机,这条消息就彻底丢 失了。

总结:redis 的过期订阅相比于其他方案没有太大的优势,在实际生产环境中,用得相 对较少。

4 Redisson 分布式延迟队列

Redisson 是一个基于 redis 实现的 Java 驻内存数据网格,它不仅提供了一系列的分 布式的 Java 常用对象,还提供了许多分布式服务。

Redisson 除了提供我们常用的分布式锁外,还提供了一个分布式延迟队列 RDelayedQueue,他是一种基于 zset 结构实现的延迟队列,其实现类是 RedissonDelayedQueue。

// 1. 获取一个目标阻塞队列(目的地)
RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque("my-queue");
// 2. 获取一个延迟队列,并绑定到上面的目标队列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
// 投递一个消息,指定30秒后才会被消费者看到
delayedQueue.offer("这是一个延迟任务", 30, TimeUnit.SECONDS);
// 再投递一个5秒后执行的任务
delayedQueue.offer("紧急任务", 5, TimeUnit.SECONDS);
// 通常在一个单独的线程中循环执行
while (true) {
    // 这里会阻塞,直到有到期消息被转入队列
    String task = blockingDeque.take();
    // 处理到期的任务
    System.out.println("处理任务: " + task);
}

优点:使用简单,并且其实现类中大量使用 lua 脚本保证其原子性,不会有并发重复 问题。

缺点:需要依赖 redis(如果这算一种缺点的话)。

总结:Redisson 是 redis 官方推荐的 JAVA 客户端,提供了很多常用的功能,使用 简单、高效,推荐大家尝试使用。

5 MQ 延迟消息

消息队列实现方式支持精度优点缺点
RocketMQ固定延迟级别18个固定级别实现简单,性能好不支持任意时间延迟
RabbitMQ死信队列+TTL任意时间灵活,支持任意延迟配置复杂,大量消息时性能问题
Kafka时间轮算法任意时间高吞吐量需要自行实现,复杂度高
// 生产者
public class RocketMQDelayProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("delay_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        
        Message msg = new Message("ORDER_TIMEOUT_TOPIC", 
            "订单超时".getBytes(StandardCharsets.UTF_8));
        
        // 设置延迟级别:3对应10秒
        msg.setDelayTimeLevel(3);
        
        producer.send(msg);
        producer.shutdown();
    }
}

// 消费者
public class RocketMQDelayConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("ORDER_TIMEOUT_TOPIC", "*");
        
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.printf("收到延迟消息: %s, 实际延迟: %dms%n",
                    new String(msg.getBody()),
                    System.currentTimeMillis() - msg.getBornTimestamp());
                
                // 处理订单超时逻辑
                handleOrderTimeout(new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        
        consumer.start();
    }
    
    private static void handleOrderTimeout(String orderInfo) {
        // 订单超时处理逻辑
        System.out.println("处理订单超时: " + orderInfo);
    }
}