本文来源:微信公众号 淘系技术
作者| 平勇(润辰)编辑| 橙子君
出品| 阿里巴巴新零售淘系技术
原文地址:https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650410759&idx=1&sn=b750e3225fa76b6ac5b84fb41a870a48
平时大家的工作中应该会遇到较多需要在某个时间点执行某个任务,比如对运维来说,定时数据库的备份,日志和监控信息的抓取;比如业务系统,某个时间点给某个人群用户发放优惠券,甚至从操作系统角度,人机交互进程、视频播放的实时进程、批处理的后台进程等进程间的调度。。。
所以如何将这些任务高效、精准的调度?是任务调度系统中最重要的命题,当然在业务系统中一个完善的任务调度系统是很复杂的,需要具备能调度、可视化管理、过程可追溯、结果可分析、持久化、高可用等特性,这篇文章主要讨论任务调度逻辑,其余的内容我们后面文章探讨。
背景
最近的一次招聘面试中,候选人介绍了他基于时间轮算法实现的一套任务调度系统,在我的个人历史工作中简单的场景会直接使用java自带的工具类,如Timer、 ScheduledThreadPool、DelayQueue 配合线程池等方法 ,复杂的一些场景用开源工具包Quartz、集团的SchedulerX等,但一直没有对这些工具的底层实现逻辑做过深入的学习,此次的沟通引起了我的兴趣。
我们可以将业务系统中需要使用定时任务调度的总结成以下三种场景:
- 时间驱动处理场景:如整点发送优惠券,每天定时更新收益,每天定时刷新标签数据和人群数据
- 批量处理数据:如按月批量统计报表数据,批量更新某些数据状态,实时性要求不高
- 异步执行解耦:如先反馈用户操作状态,后台异步执行较耗时的数据操作,以实现异步逻辑解耦
所以从时间维度可以分为定时或者延迟一定时间两个维度,而时间轮就是一种高效的利用线程资源来进行批量化调度的一种调度模型,把大批量的调度任务全部都绑定到同一个的调度器上面,使用这一个调度器来进行所有任务的管理,触发以及运行,能够高效的管理各种延时任务,周期任务。
这篇文章主要有三块内容:
1.JAVA自带的工具类的内部实现逻辑是什么?
2.时间轮算法原理?
3.时间轮算法有哪些应用案例?(Netty、Kafka案例)
Timer、ScheduledThreadPool、DelayQueue实现逻辑
▐ Timer
Timer 可以实现延时任务,也可以实现周期性任务。我们先来看看 Timer 核心属性和构造器。
private final TaskQueue queue = new TaskQueue();//基于数组实现的优先队列,用作存放任务private final TimerThread thread = new TimerThread(queue);//执行任务线程public Timer(String name) { thread.setName(name); thread.start(); //构造时默认启动线程 }
Timer核心就是一个优先队列和封装的执行任务的线程,从这我们也可以看到一个 Timer 只有一个线程执行任务,再来看看是如何实现延时和周期性任务的。
我先简单的概括一下:首先维持一个小顶堆,即最快需要执行的任务排在优先队列的第一个,根据堆的特性我们知道插入和删除的时间复杂度都是 O(logn),然后TimerThread不断地拿排在第一个任务的执行时间和当前时间做对比,如果时间到了先看看这个任务是不是周期性执行的任务?如果是则修改当前任务时间为下次执行的时间,如果不是周期性任务则将任务从优先队列中移除,最后执行任务。如果时间还未到则调用 wait() 等待(参考以下流程图)。

再看下代码逻辑,先来看下 TaskQueue,主要看下插入任务的过程,特别是插入时间复杂度部分。
class TaskQueue { private TimerTask[] queue = new TimerTask[128]; void add(TimerTask task) { // Grow backing store if necessary if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); //扩容 queue[++size] = task; //先将任务添加到数组最后面 fixUp(size); //调整堆 } private void fixUp(int k) { //时间复杂度为O(logn) while (k > 1) { int j = k >> 1; if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)//通过任务执行时间对比,调整顺序 break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } /** * Return the "head task" of the priority queue. (The head task is an * task with the lowest nextExecutionTime.) */ TimerTask getMin() { return queue[1]; //返回最接近执行时间的任务 } //.......}
再来看看 TimerThread 的 run 操作。
public void run() { try { mainLoop();//无异常捕获 } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } } /** * The main timer loop. (See class comment.) */ private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; task = queue.getMin(); //获取任务 synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { //取消泽移除并继续循环 queue.removeMin(); continue; // No action required, poll queue again } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) { //执行时间到了 if (task.period == 0) { // 不是周期任务 queue.removeMin(); //移除任务 task.state = TimerTask.EXECUTED;//变更任务状态为已执行 } else { // 周期任务,更新时间为下次执行时间 queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } if (!taskFired) // 还未到达执行时间等待 queue.wait(executionTime - currentTime); } if (taskFired) // 执行任务,无异常捕获 task.run(); } catch(InterruptedException e) { } } }
小结:
可以看出Timer实际就是根据任务的执行时间维护了一个优先队列,并且起了一个线程不断地拉取任务执行,根据代码可以看到有三个问题:
- 优先队列的插入和删除的时间复杂度是O(logn),当任务量大的时候,频繁的入堆出堆性能有待考虑
- 单线程执行,如果一个任务执行的时间过久则会影响下一个任务的执行时间(当然你任务的run要是异步执行也行)
- 从代码中可以看到对异常没有做什么处理,那么一个任务出错的时候会导致之后的任务都无法执行
▐ ScheduledThreadPoolExecutor
在看Timer源码时,看到了以下一段注释:
/** * ....... *
Java 5.0 introduced the {@code java.util.concurrent} package and * one of the concurrency utilities therein is the {@link * java.util.concurrent.ScheduledThreadPoolExecutor * ScheduledThreadPoolExecutor} which is a thread pool for repeatedly * executing tasks at a given rate or delay. It is effectively a more * versatile replacement for the {@code Timer}/{@code TimerTask} * combination, as it allows multiple service threads, accepts various * time units, and doesn't require subclassing {&#