调度中心它是一个springboot项目,直接运行 XxlJobAdminApplication 即可。程序在启动的时候会扫描到XxlJobAdminConfig。这个类实现了InitializingBean接口
所以经过Bean的生命周期,一定会调用afterPropertiesSet这个方法的实现
任务调度
JobScheduleHelper.getInstance().start();
在这一句代码里面主要是启动了2个守护线程(见JobScheduleHelper):
一个是scheduleThread,负责不断地从数据库中查询到点触发的jobId,然后存放到一个map中,这个线程我们称之为调度线程
另一个是ringThread,不断地这个map中取出jobId(代表一个任务),交给快慢线程池(二线一)去执行,这个线程我们称之为 响铃线程
流程图我已经上传到gitee上了
调度线程scheduleThread
我们先来看看调度线程它是如何选择触发任务的
每次在轮询的时候到底一次性从db中查找多少条job合适?
xxl-job计算job的预读数代码如下:
int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax()
+ XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
我们知道调度线程只是负责从db中筛选满足触发条件的job,然后交给快慢线程池去执行,所以我们需要考虑快慢线程池的负载能力
快线程池中的最大线程数默认为200,慢线程池中的最大线程数默认为100,有的JobA在快线程池中运行,有的JobB在慢线程池中运行,所以理论上两者创建的最大线程池数总共可达到300
快慢线程池中执行的主要业务逻辑就是:XxlJobTrigger.trigger(),每次执行大概花费50ms(xxl提供的),所以qps就是20(每秒处理20次trigger)
所以preReadCount其实就是在1秒钟的时间内,最多能真正触发的job数,最多为(200 + 100) * 20 = 6000
尝试加锁
加锁核心代码如下(移除移除处理和无用日志)
Connection conn = null;
Boolean connAutoCommit = null;
PreparedStatement preparedStatement = null;
try {
conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
preparedStatement = conn.prepareStatement("select * from xxl_job_lock where lock_name = 'schedule_lock' for update");
preparedStatement.execute();
// 尝试加锁成功
// 继续后续操作
} finally {
if (conn != null) {
conn.commit();
conn.setAutoCommit(connAutoCommit);
conn.close();
}
if (null != preparedStatement) {
preparedStatement.close();
}
}
其实这些代码就是为了执行一句sql:
select * from xxl_job_lock where lock_name = 'schedule_lock' for update
为什么执行这句sql呢?原因如下:
由于调度中心可以是集群的形式,每个调度中心实例都有调度线程,那么如何保证任务在同一时间只会被其中的一个调度中心触发一次?
XxlJob直接基于数据库来实现的分布式锁的
在调度之前,调度线程会尝试执行下面这句sql:select * from xxl_job_lock where lock_name = 'schedule_lock' for update
一旦执行成功,说明当前调度中心成功抢到了锁,接下来就可以执行调度任务了
当调度任务执行完之后再去关闭连接,从而释放锁
由于每次执行之前都需要去获取锁,这样就保证在调度中心集群中,同时只有一个调度中心执行调度任务
响铃线程ringThread
核心代码如下(移除了日志打印和异常处理):
while (!ringThreadToStop) {
// 睡眠0~1s
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
List<Integer> ringItemData = new ArrayList<>();
// 获取当前秒
int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
for (int i = 0; i < 2; i++) {
// 当 i = 0 时,sec = nowSecond
// 当 i = 1 时,sec = nowSecond - 1 向前减一秒(避免处理耗时太长,跨过刻度,向前校验一个刻度;)
int sec = (nowSecond + 60 - i) % 60; // + 60 是为了防止sec是个负数
List<Integer> tmpData = ringData.remove(sec);
if (tmpData != null) {
ringItemData.addAll(tmpData);
}
}
if (ringItemData.size() > 0) {
for (int jobId : ringItemData) {
// 交给快慢线程池(二线一)处理job
JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
}
ringItemData.clear();
}
}
ringThread不断地从ringData中读取出这一秒和上一秒要触发的所有jobId(为什么要额外取出上一秒的jobId,以上代码已经附注释),然后调用JobTriggerPoolHelper.trigger()交给快慢线程池(二线一)处理job。
ringData其实是一个hashMap,key主要是当前秒,value是这一秒对应要触发的所有jobId
// key:当前秒
// value:这一秒要触发的所有jobId
private volatile static Map<Integer, List> ringData = new ConcurrentHashMap<>();