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