如何构建延迟任务调度系统(二):技术调研

延时队列实现的几种方式

  • java.util.Timer + java.util.TimerTask
  • java.util.concurrent.ScheduledExecutorService
  • Quartz
  • java.util.concurrent.DelayQueue
  • 数据库轮询
  • redis过期键通知
  • rocketMQ中的延时队列

1. Timer+TimerTask

使用 Timer 实现任务调度的核心类是 Timer 和 TimerTask。其中 Timer 负责设定 TimerTask 的起始与间隔执行时间。使用者只需要创建一个 TimerTask 的继承类,实现自己的 run 方法,然后将其丢给 Timer 去执行即可

Timer 的设计核心是一个 TaskList 和一个 TaskThread。Timer 将接收到的任务丢到自己的 TaskList 中,TaskList 按照 Task 的最初执行时间进行排序。TimerThread 在创建 Timer 时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread 被唤醒并执行该任务。之后 TimerThread 更新最近一个要执行的任务,继续休眠。

实现思想:应用维护一个全局的Timer调度器,延时任务实现TimerTask,run方法中实现逻辑。计算好具体的延迟执行时间,交给Timer去调度。

选型评估:简单易用,但是缺点较多,单线程调度,所有任务都是串行的,性能低,前一个任务的延迟或异常都将会影响到之后的任务,影响实时性,同时也不具备延时队列的几点能力

3.2 ScheduledExecutorService

基于Timer的缺陷,JDK5推出了基于线程池设计的 ScheduledExecutor,原理是
每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。

选型评估:引入多线程,解决了Timer的一些缺点,但是只适合单机,分布式环境不支持。也不具备延时队列的几点能力,需要考虑跟别的技术结合使用评估是否可以满足延时队列的能力。

3.3 Quartz

Quartz是个轻量级的任务调度框架,可以跟多个应用集成,并且具有容错机制,重启服务的时候内存中丢失的任务可以被持久化

选型评估:

  • Quartz满足了我们需要的延时队列的可靠性: 持久化任务,避免了服务重启的时候内存中的任务丢失,高可用:执行任务的节点挂了,另外的节点会继续执行
  • 集群分布式并发环境中使用QUARTZ定时任务调度,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务,Quartz的任务触发只能在单个节点运行,其它节点不执行任务,性能低,浪费资源

3.4 DelayQueue

DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。

选型评估:

  • 效率高,任务触发时间延迟低
  • 适用于单机,需要结合其他技术运用,
  • 数据是保存在内存,需要自己实现持久化
  • 不具备分布式能力,需要自己实现高可用

3.5 数据库轮询

每隔一段时间去查询数据库,处理好的记录标记状态

选型评估:定期轮询数据量大的时候会消耗太多IO资源,效率低

3.6 redis过期键通知

需要DBA做一些额外的配置,开启这个功能

选型评估:Redis的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了

7. rocketMQ中的延时队列

选型评估:rocketMQ中消息延迟时间为固定时间段:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,粒度不够,不能很好支持业务

总结

延时队列的技术点还有很多,比如说时间轮之类的方案,要满足延时队列的几点特性,实现高可用,可靠性,我们需要结合多个技术去实现。

你可能感兴趣的:(业务架构)