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队列的话一天天去轮询会有大量的无效消息。