延迟队列技术方案选型(想到再补充...)

877 阅读4分钟

1. 场景

越来越多的业务场景需要用到延迟队,比如:

  • 任务调度:将来某个【特定时刻】执行的任务放入队列。
  • 消息延迟处理:在消息推送模块,将消息放入延迟队列,并在一段时间后进行重新处理。
  • 订单处理:在我们项目中,对接XXX微信支付的过程中,文档中要求在订单【明确支付失败】的情况下需要对订单进行关闭。
  • 缓解高峰期负载:在高峰期,系统可能会面临大量的请求和负载,延迟队列可以用于请求一些推迟处理,进行【削峰处理】。比如我们的消息推送模块,待结算推送是提供给his系统对接的,第三方可能会一直请求接口,那么我们接入层可以用延迟队列进行接收。

2. 技术方案

我暂时想到的会有以下几种技术方案:

  • JDK的工具包:延迟队列 delayQueue
  • 任务调度框架: XXL-Job
  • Redis :Zset
  • 消息队列 :RocketMQ 提供的延迟队列

3. 场景考虑

对以上技术方案做一些简单的分析考量分析,看看我们适合哪些。

3.1. JDK DelayQueue

这个是JDK自带的DelayQueue延迟队列,可以实现。

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class MyDelayedTask implements Delayed {
       private final long delayTime;
       private final long expireTime;

       public MyDelayedTask(long delay, TimeUnit timeUnit) {
           this.delayTime = timeUnit.toMillis(delay);
           this.expireTime = System.currentTimeMillis() + this.delayTime;
       }

       @Override
       public long getDelay(TimeUnit unit) {
           return unit.convert(this.expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
       }

       @Override
       public int compareTo(Delayed o) {
           if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
               return -1;
           } else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
               return 1;
           }
           return 0;
       }
   }


 import java.util.concurrent.DelayQueue;

   public class DelayQueueExample {
       public static void main(String[] args) {
           DelayQueue<MyDelayedTask> queue = new DelayQueue<>();
           queue.put(new MyDelayedTask(5, TimeUnit.SECONDS)); // 添加一个5秒后到期的任务
           queue.put(new MyDelayedTask(2, TimeUnit.SECONDS)); // 添加一个2秒后到期的任务

           while (!queue.isEmpty()) {
               try {
                   MyDelayedTask task = queue.take(); // 这将阻塞直到有任务到期
                   System.out.println("Processing task: " + task);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   break;
               }
           }
       }
   }

DelayQueue是一个基于JVM的无界队列,如果订单量大的话,会导致OOM,机器重启,里面的数据也会没掉。所以数据量小的话,单体的应用可以考虑。

3.2. XXL-Job

定时任务的框架,通过这些调度框架去执行任务。

  • 这些定时任务是基于【固定的频率】去调度【任务方法】的,会出现有些订单已经支付失败了(我们需要取调用关闭订单的操作),但是定时任务的调度的时间还没到(这时候会存在一个【时间差】),好在我们的订单业务要求关闭的订单对这个时间要求不高可以使用。
  • 如果订单数据量大的,触发这个任务时,会扫描表,对数据库造成压力,导致数据库IO在短时间大量占用和消耗,会影响线上业务。针对这种情况,我们会对订单状态【加上索引】。因为数据量大的情况下 支付失败区分度还是比较高的,扫表的效率可以提高。其次也可以考虑多线程并发扫表。
3.3. Redis Zset
  • zadd key score(时间) member(trace_id) : 向有序集合 key 中添加一个或者多个成员 每个成员 都带有一个分值 score。

  • zrangebyscore key min max :返回有序集合key中分值介于 min max之间的成员。

  • zrem key member : 从有序集合key中移除 一个或者多个成员。

使用 zset + 定时轮询器,该方案 可以借助 【Redis 的持久化】,高可用机制,避免数据丢失。

这个方案的弊端就是说定期检查任务中是否需要被处理,会占用cpu资源。

3.4. RocketMQ

RocketMQ有这个延迟消息的功能,当消息写入Broker之后,不会被消费者立刻消费,需要等待指定的时长后才可以被消费。但不是任意的时长:1s 5s 10s 30s 1m 2m 3m 4m.....10m 20m 30m 1h 2h。

4. 总结

单体应用 业务量不大 可以选择 JDK方案。

分布式应用 业务量不大 Redis方案。

分布式应用 业务量比较大 Redis、RocketMQ方案。

总体考虑的话

  • 考虑到成本 复杂度 个人的话 优先 选择 Redis、定时任务、RocketMQ ,因为Redis每个业务都有用到。
  • 像下单完查询订单状态的业务避免支付平台未回调的场景 以及 接受外部系统调用我们系统去对接消息推送的场景我会选择 Redis RocketMQ。
  • 像遇到订单关闭的场景,我会选择XXL-JOB去轮询表的方案,因为订单支付的失败的订单数量相对较少,如果都放入 Redis、RocketMQ队列的话一天天去轮询会有大量的无效消息。