文章目录
大家好,我是被白菜拱的猪。
一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。
XXL-JOB
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
设计思想
设计思想就是将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,业务逻辑都在执行器那,“调度中心”负责发起调用请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的 JobHandler 中业务逻辑。
因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。
系统组成
- 调度模块(调度中心):
负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。 - 执行模块(执行器):
负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
接收“调度中心”的执行请求、终止请求和日志请求等。
总结介绍
XXL-JOB是一个分布式任务调度平台,开发迅速、学习简单、轻量级、易扩展。
它的整体架构分为两大模块:调度模块和执行模块,也就是调度中心和执行器,它的设计思想就是就是将调度和任务进行解耦,从而提升系统的稳定性和扩展性。
调度中心主要是负责管理调度信息,按照调度配置发出调度请求,自身是不承担业务代码。调度系统与任务进行解耦,提高了系统可用性和稳定性,调用系统性能不再受限于任务模块。支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除。同时支撑监控监督结果以及执行日志,支持执行器Failover(故障转移)。
执行器则负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求。
我们采用Bean模式下的方法模式,使用@XxlJob注解去写我们的任务。
源码分析
下面从一个定时任务是如何执行的开始进行解析。
1、调度中心
首先是从 XxlJobAdminConfig 这个配置类出发 ,它实现了InitializingBean 这个Bean,所以Spring容器在初始化的时候会调用aferPropertiesSet这个方法,在这个方法中创建了XxlJobScheduler,调用了它的init方法,所有关于调度中心的都是在这个方法中进行初始化。
public void init() throws Exception {
// init i18n
initI18n();
// admin registry monitor run
JobRegistryMonitorHelper.getInstance().start();
// admin fail-monitor run
JobFailMonitorHelper.getInstance().start();
// admin lose-monitor run
JobLosedMonitorHelper.getInstance().start();
// admin trigger pool start
JobTriggerPoolHelper.toStart();
// admin log report start
JobLogReportHelper.getInstance().start();
// start-schedule
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
第一个是国际化相关、监控、失败重试、触发器注册接收注册请求、日志。
第五步JobScheduleHelper调度器,死循环,在xxl_job_info表里取将要执行的任务,更新下次执行时间的,调用JobTriggerPoolHelper类,来给执行器发送调度任务的。
不同的模块都是新启一个线程池,所以互不影响,我们主要是看如果执行调度任务的,他是一个死循环。
在这个方法中主要维护了两个线程schedule thread和ring thread,在这两个线程当中都是使用while循环来执行任务。
schedule thread
因为调用中心有可能是集群部署,那么就有可能会有多个调用中心去执行任务,所以这里涉及到分布式锁的问题,他的解决方案是从数据库层面出发,单独有一个xxl_job_info 表,表很简单就一个字段,lock_name.使用for update语句来进行上锁,然后设置的是不自动提交,当其他线程执行任务时就会在数据库层面进行阻塞,这就保证下面执行的任务都是在同一事务下。
select * from xxl_job_lock where lock_name = 'schedule_lock' for update
然后开始从xxl_job_info表中查询要执行的调度任务,查询条件为下一次任务执行时间小于等于当前时间+5秒预留时间,也就是说把未来5s要执行的时间也查询出来了,为什么要预留五秒呢?假如我们第10秒有个任务要执行,那么不可能等到第10秒的时候才去执行sql语句吧,执行的时候老早就过了时间了,所以得提前查出来。
对要执行的任务进行遍历,有三个分支。方便理解我们可以分为三段0-5-10-15
0-5,5-10,10-15。当前时间为10秒。
第一个分支,下一次时间+5秒预留时间 < 当前时间 ,也就是0-5这个时间段,表示当前任务触发时间超过5秒,已经超时,则不再执行,更新下次任务执行时间。
// time-ring jump
if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
// 2.1、trigger-expire > 5s:pass && make next-trigger-time
logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());
// fresh next
refreshNextValidTime(jobInfo, new Date());
第二个分支,当前时间大于下一次触发时间,也就是10-15这个区间,这时候就使用触发器立即执行任务,然后更新下一次执行时间,假如当前时间+5s大于下一次执行时间,也就是在这个阶段也是要执行的,则把他加入到ringData里面,ringData是要在ring thread里面要执行的任务,是一个concurrentHashMap,key是要执行的时间,value是该时间要执行的jobId,比如在第10秒,有id为1,2,3的任务要执行,所以是list。
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
第三个分支也就是未来五秒内要执行的任务,也是要放进时间轮时间ring thread。他们都是一秒一秒去执行。
而且两个线程最后都是调用JobTriggerPoolHelper,trigger执行器去执行任务,在trigger里面有两个线程池,fastTriggerPool和slowTriggerPool,用来处理不同的任务,用来降低任务与任务之间的影响,比如超时次数大于10,则交给慢线程池去执行。
调度线程池隔离,拆分为”Fast”和”Slow”两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性;
ring thread
ring thread根据当前秒数刻度和前一个刻度进行时间轮的任务获取,每次取完就remove,比如当前时间为10秒,那他从ringData取的就是10秒和9秒,这样是为了避免处理耗时太长,跨过刻度,所以向前校验一个刻度;
常见定时任务实现方式
1、Timer
优点:
是JDK自带的定时任务执行类
缺点:
1、当一个任务执行时间过长,会影响其他任务。比如当任务 1 运行时间超过设定的间隔时间时,任务 2 也会延迟执行。 原本任务 1 和任务 2 的执行时间间隔都是 3s,但因为任务 1 执行了 5s,因此任务 2 的执行时间间隔也变成了 10s(和原定时间不符)。
2、当一个任务报异常时,其他任务也会终止,所以在生产环境谨慎使用。
2、ScheduledExecutorService
ScheduledExecutorService 也是 JDK 1.5 自带的 API,我们可以使用它来实现定时任务的功能,ScheduledExecutorService 可以实现 Timer 类具备的所有功能,并且它可以解决了 Timer 类存在的所有问题。
底层是使用了延迟队列,DelayQueue,而延迟队列的底层又是优先级队列PriorityQueue。
他有两个方法,一个固定频率一个是固定延迟时间。
在单机生产环境下建议使用 ScheduledExecutorService 来执行定时任务,它是 JDK 1.5 之后自带的 API,因此使用起来也比较方便,并且使用 ScheduledExecutorService 来执行任务,不会造成任务间的相互影响。
3、SpringTask
使用上面两种定时任务的实现方式,很难实现设定了具体时间的定时任务,比如当我们需要每周五来执行某项任务时。
@EnableScheduling
@Scheduled
以上都是单机的
分布式定时任务比较
向分布式定时任务,我们熟悉的主要有Quartz和XXL-JOB。
Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:
- 问题一:调用API的的方式操作任务,不人性化;
- 问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。
- 问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
- 问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。
XXL-JOB弥补了quartz的上述不足之处。
比如我们想改时间就必须要修改源代码,非常麻烦。而且也不能手动的去停止定时任务。