这是我参与「第五届青训营 」伴学笔记创作活动的第24天。主要内容为定时任务的发展历程、分布式定时任务的实现原理和使用分布式定时任务的业务场景,在电商(订单超时取消)、互动(支付宝集五福)、游戏(定期更新游戏榜单) 等场景下都可以考虑使用分布式定时任务。
单机定时任务
- 操作系统内部命令:Linux系统下的
CronJob命令;windows的任务计划程序,仅能在指定操作系统下执行 - 程序语言计时器:Java的
timer,Go的ticker,可跨平台但也仅限于单机使用
\\Java定时任务
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run(){
SyncLocalCache();
}
}, 5000, 5*60*1000);
}
\\Go定时器
func main() {
ticker := time.NewTicker(5 * time.Minute)
for {
select{
case <- ticker.C:
SyncLocalCache()
}
}
}
- 单机定时任务:
ScheduledExecutorService,Java的定时任务类,是基于线程池实现的,当调度任务来的时候才会启动一个线程,其余时间都是处于轮询任务的状态 - Java的作业调度框架
Quartz,能够为他来执行一个作业而创建简单的或复杂的调度
分布式定时任务的整体架构
分布式定时任务核心要解决触发、调度、执行三个问题;因此由以下三大模块构成
- 触发器:Trigger,解析任务,生成触发事件
- 调度器: Scheduler, 分配任务,管理任务生命周期
- 执行器:Executor,获取执行任务单元,执行任务逻辑 除此之外,还需要一个控制台(Admin)来提供任务管理和干预的功能。
触发器
触发器的核心职责是给定一系列任务,解析它们的触发规则,在规定的时间点触发任务的调度,设计时需要考虑以下约束:
- 需支持大量任务
- 需支持秒级的调度
- 周期任务需多次执行
- 需保证秒级扫描的高性能,避免资源浪费 有两种实现方案
- 定期扫描+延时消息:定时扫描的机器集部署,通过分布式锁保证只有一台在调度。
- 时间轮:时间轮是一种高效利用线程资源进行批量化调度的一种调度模型,用一个环形队列存储,底层用数组来实现,每个元素存放一个定时任务列表。
时间轮算法的演进
简单的单时间轮:
在这种数据结构下添加、删除、执行定时任务的时间复杂度都为O(1),但是单时间轮的定时任务到期时间是有限的,无法满足业务需求,如果到期时间的范围比较大,那么数组的开销也很大。
改进版单时间轮: 每个元素不单单挂在到期时间下的定时任务,还可以挂在下一轮的任务,如下图所示:
每个任务节点还存储着代数,表示时针转动几轮后执行该任务,属于是时间和空间的一个折中方案,不需要过大的数组单也失去了O(1)的时间复杂度,这种改进时间轮算法如果某个元素上挂载的定时器特别多,则需要花费大量时间去遍历这些节点,如果节点的iter都不同,则一轮下来只有少数的定时器需要立即执行。
多级时间轮: 用多个到期时间不同的时间轮来实现仅用较少的刻度表示大范围度量值的效果,如下图所示:
每一级的轮子都有一个指针,规律是次级时间轮一个周期的时间是上一级一个元素的时间,类似时针分针秒针的关系,低级时间轮走一圈,高级时间轮走一个元素。
调度器
调度器需要考虑三个问题资源来源、资源调度、任务执行;
资源来源: 有以下两种来源
- 业务系统提供机器资源:优点是任务执行逻辑与业务系统共用一份资源,利用率更高,但是也容易发生定时任务脚本影响在线服务的事故且不能由定时任务平台控制扩缩容;
- 定时任务平台提供机器资源:优点是任务执行逻辑和业务系统提供的在线服务隔离,可以支持扩缩容,但是需要消耗更多的机器资源,需要额外为定时任务平台申请接口调用权限;
资源调度: 也就是节点的选择,有三种策略
- 随机节点执行:选择集群中一个可用的执行节点执行调度任务,使用场景:定时对账;
- 广播执行:在集群中所有的执行节点分发调度任务并执行。适用场景有批量运维;
- 分片执行:按照用户自定义分片逻辑进行拆分,分发到集群中不同节点并执行,比如MapReduce模型,适用于海量数据处理的场景。