03 xxl-job调度中心

244 阅读4分钟

调度中心它是一个springboot项目,直接运行 XxlJobAdminApplication 即可。程序在启动的时候会扫描到XxlJobAdminConfig。这个类实现了InitializingBean接口

所以经过Bean的生命周期,一定会调用afterPropertiesSet这个方法的实现

image.png

任务调度

JobScheduleHelper.getInstance().start();

在这一句代码里面主要是启动了2个守护线程(见JobScheduleHelper):

一个是scheduleThread,负责不断地从数据库中查询到点触发的jobId,然后存放到一个map中,这个线程我们称之为调度线程

另一个是ringThread,不断地这个map中取出jobId(代表一个任务),交给快慢线程池(二线一)去执行,这个线程我们称之为 响铃线程

image.png

流程图我已经上传到gitee上了

image.png

调度线程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<>();

快、慢线程池