分布式定时任务系列7:XXL-job源码分析之任务触发

 传送门

分布式定时任务系列1:XXL-job安装

分布式定时任务系列2:XXL-job使用

分布式定时任务系列3:任务执行引擎设计

分布式定时任务系列4:任务执行引擎设计续

分布式定时任务系列5:XXL-job中blockingQueue的应用

分布式定时任务系列6:XXL-job触发日志过大引发的CPU告警

 Java并发编程实战1:java中的阻塞队列

为何要看源码

DB日志

在上一节,谈到了由于XXL-job触发日志过大引发了CPU告警,主要是由于XXL-job将触发日志记录到了Mysql数据库,xxl_job_log表中:

CREATE TABLE `xxl_job_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
  `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
  `trigger_code` int(11) NOT NULL COMMENT '调度-结果',
  `trigger_msg` text COMMENT '调度-日志',
  `handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
  `handle_code` int(11) NOT NULL COMMENT '执行-状态',
  `handle_msg` text COMMENT '执行-日志',
  `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
  PRIMARY KEY (`id`),
  KEY `I_trigger_time` (`trigger_time`),
  KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

但是理论上,这个设计是有一定的隐患:

  • 从时间纬度上:随着程序运行,周期性任务对应的触发日志数据量会不断的增大
  • 从数量纬度上:业务可能会不断丰富,增加许多新任务

而众所周知,Mysql对于单表的数据量是有一定限制的,超过Kw数据级性能会严重下降,甚至会影响正常的运行(这个可以参考实操测试章节)。而阿里巴巴开发手册也有Mysql使用规范:

文件日志

除了XXL-job的管理端记录的db触发日志之外,XXL-job在客户端(执行器)也记录了文件日志,并且是自定义的日志系统,不能进行配置,也无法关闭,也可能会导致客户端服务器存储占用过大,具体代码在com.xxl.job.core.thread.JobThread#run:

一路再跟进XxlJobHelper.log方法,可以看到会创建本地文件,而且是触发一次,生成一个:

所以有些问题,碰到了之后都可以通过阅读源码:

  • 了解它的设计思想,理念,以及设计模式
  • 问题产生的原因,以及框架本身提供解决方案
  • 本身设计的不足及局限
  • 修改源码实现定制化逻辑及二开,比如修改过nacos默认的认证:接入当前公司的认证体系

源码目录

XXL-job的源码目录在官网有介绍,具体可参考如下:

触发整体时序图

这里,还是以前面的auth模块做例子,进行debug源码跟踪:通过debug可以验证许多猜想及数据流的转换。

在此之前,先从整体上梳理一下任务触发的调用逻辑,通过一个时序图来展现:

上面第3步里面com.xxl.job.admin.core.scheduler.XxlJobScheduler#init,各种资源Helper初始化,包括很多类型:

public void init() throws Exception {
        // 国际化
        initI18n();

        // admin端任务触发线程池启动
        JobTriggerPoolHelper.toStart();

        // admin端注册监控启动
        JobRegistryHelper.getInstance().start();

        // admin端失败监控启动,失败进行邮件告警
        JobFailMonitorHelper.getInstance().start();

        // admin端,任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
        JobCompleteHelper.getInstance().start();

        // admin端日志处理统计启动:日志统计+日志清理
        JobLogReportHelper.getInstance().start();

        // 定时任务触发启动
        JobScheduleHelper.getInstance().start();

        logger.info(">>>>>>>>> init xxl-job admin success.");
    }

定时任务触发启动

这里面重点看下一下定时任务触发启动这个类,跟进start()方法:

{

        // 立即创建一个线程
        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init xxl-job admin scheduler success.");

                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

                // 不停扫描任务表xxl_job_info,相当于线程的自旋
                while (!scheduleThreadToStop) {

                    // Scan Job
                    long start = System.currentTimeMillis();

                    Connection conn = null;
                    Boolean connAutoCommit = null;
                    PreparedStatement preparedStatement = null;

                    boolean preReadSuc = true;
                    try {
                        // JDBC操作数据库
                        conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);
                        
                        // 加上db悲观锁,防止并发执行
                        preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                        preparedStatement.execute();

                        // tx start

                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        // 查询所有任务列表,一次最多6000个
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {

                                // 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());

                                    // 1、错过触发时间,根据策略决定,是否立即补尝执行一次
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                                        // FIRE_ONCE_NOW 》 trigger
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }

                                    // 2、更新下次执行相关时间参数
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、触发任务
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、更新下次执行相关时间参数
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

                            // 3、更新db表中,下次执行相关时间参数
                            for (XxlJobInfo jobInfo: scheduleList) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }

                        } else {
                            preReadSuc = false;
                        }

                        // tx stop


                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {

                        // commit
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

                        // close PreparedStatement
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    long cost = System.currentTimeMillis()-start;


                    // Wait seconds, align second
                    if (cost < 1000) {  // scan-overtime, not wait
                        try {
                            // pre-read period: success > scan each second; fail > skip this period;
                            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                        } catch (InterruptedException e) {
                            if (!scheduleThreadToStop) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
            }
        });
        // 线程启动
        scheduleThread.setDaemon(true);
        scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
        scheduleThread.start();
    }

DB锁防"并发"

上面的start()方法里面,有一行代码:

preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                        preparedStatement.execute();

有用过此方法的应该都不陌生,这就是Mysql里面悲观锁的用法:对指定的记录添加X锁(具体是行锁,表锁视隔离级别+索引而定,可参考:MySQL锁系列(3):悲观锁探究)。但是问题是这里为什么要加db锁呢?或者说对admin端来说,为什么要加这样的重量级"分布式"锁呢?来看一下官网的一段话:

  • 23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行;

从这里,就不难猜到,这行代码是解决在集群部署,形成的分成式环境下,多个定时任务线程同时触发的问题:通过施加DB锁,保证同一时刻,只有一个线程在执行任务触发,从而避免调度任务的重复执行。也可以通过db脚本来看看,xxl_job_lock表的记录:

INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');

只有一条记录,就是为这里的定时任务触发服务的。

友情提示

如果要使用db悲观锁,要开启事物,XXL-job的代码里面也是的,看start()方法:

conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);

而最后,也在finally块里面进行了释放:

// commit
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

执行时间刷新

上面有一个重要的方法,refreshNextValidTime用来进行更新定时任务的下次执行时间:

private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
        Date nextValidTime = generateNextValidTime(jobInfo, fromTime);
        if (nextValidTime != null) {
            jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
            jobInfo.setTriggerNextTime(nextValidTime.getTime());
        } else {
            jobInfo.setTriggerStatus(0);
            jobInfo.setTriggerLastTime(0);
            jobInfo.setTriggerNextTime(0);
            logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}",
                    jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf());
        }
    }

继续跟进这个方法,generateNextValidTime用来获取下次执行时间:

// ---------------------- tools ----------------------
    public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
        ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
        if (ScheduleTypeEnum.CRON == scheduleTypeEnum) {
            Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime);
            return nextValidTime;
        } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) {
            return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 );
        }
        return null;
    }

看到这里,有没有一种恍然大悟的感觉呢:原来在XXL-job创建执行任务时,选择的调度策略就是在这里进行计算的。

举例说明

具体什么意思呢,以前面创建过的一个任务first_job为例,在界面进行编辑:

 可以看到调度配置--》调度类型:

  • CRON,表示CRON表达式,后面跟的就表达式配置,十分灵活
  • 固定速度,比如每隔30s执行一次,适合固定频率的任务

比如first_job配置的Cron表达式为:0/5 * * * * ?,表示每隔5s执行一次。在db任务中记录为:

INSERT INTO `xxl_job`.`xxl_job_info` (`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`) VALUES ('2', '1', 'first_job', '2023-11-25 16:46:20', '2023-11-25 16:46:32', 'kobe', '', 'CRON', '0/5 * * * * ?', 'DO_NOTHING', 'FIRST', 'first_job', '', 'SERIAL_EXECUTION', '0', '0', 'BEAN', '', 'GLUE代码初始化', '2023-11-25 16:46:20', '', '1', '1703686055000', '1703686060000');

 启动一下XX-job,打个debug跟进一下里:

而对于固定频率的计算,则更为直接,直接用当前时间+scheduleConf时间(单位为秒)即可:

if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) {
            return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 );
        }

过期处理策略

 当任务列表被捞起来之后,会循环执行任务触发。如果不符合条件,则会更新对应的执行时间字段。但是在某些异常的情况下,任务可能会错过触发:

  • 可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过;

所以,XXL-job针对这种情况代码里面做了过期处理策略:

  • 过期超5s:本次忽略,当前时间开始计算下次触发时间
  • 过期5s内:立即触发一次,当前时间开始计算下次触发时间

对应代码如下:

                                // 过期超5s
                                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());

                                    // 1、任务调度错过触发时间时的处理策略
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                                        // 立即触发一次
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }
                                   // 如果不执行if里面的trigger,就表示本次忽略


                                    // 2、当前时间开始计算下次触发时间
                                    refreshNextValidTime(jobInfo, new Date());

可以看下过期策略定义的枚举:

public enum MisfireStrategyEnum {

    /**
     * 本次忽略
     */
    DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")),

    /**
     * 立即触发一次
     */
    FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now"));
}

任务触发

上面介绍了XXL-job的过期处理策略及对应的代码,其实这仅是任务触发的一种场景:

  •  为什么叫做过期,这个超过5s是什么意思?
  • 除了过期,是不是还有未过期的:当前的,未来的?

带着这些疑问,再看下触发的代码:

if (scheduleList!=null && scheduleList.size()>0) {
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {

                                // time-ring jump:触发时间超过5s
                                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());

                                    // 1、错过触发时间,根据策略决定,是否立即补尝执行一次
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                                        // FIRE_ONCE_NOW 》 trigger
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }

                                    // 2、更新下次执行相关时间参数
                                    refreshNextValidTime(jobInfo, new Date());

                                 // 触发时间小于当前时间且在5s以内的任务
                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、触发任务
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、更新下次执行相关时间参数
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again:// 触发一次之后,下次执行时间在未来5s以内的任务
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

相信初次看到这段代码的时候,不知道有没有像我一样有些摸不着头脑(觉得写的有点乱啊,有没有同感)。

为了解释清楚这段代码,要先定义一下XXL-job的所谓预读时间环的设计概念!

预读

前面在代码里面,查询任务列表的代码如下:

任务数量
// pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

从这个源码的注释就能看出来,预读数量preReadCount就是6000:triggerPoolFastMax默认200,triggerPoolSlowMax默认为100

 preReadCount = (200 + 100) * 20 = 6000!

 查询条件
// 1、pre read
                        long nowTime = System.currentTimeMillis();
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);

其中PRE_READ_MS代码定死了就是5s中:

public static final long PRE_READ_MS = 5000;    // pre read

对应的SQL:

SELECT <include refid="Base_Column_List" />
		FROM xxl_job_info AS t
		WHERE t.trigger_status = 1
			and t.trigger_next_time <![CDATA[ <= ]]> #{maxNextTime}
		ORDER BY id ASC
		LIMIT #{pagesize}

从上面可以看出,任务查询更新都是围绕triggerNextTime字段进行的,这个字段意思就是:

private long triggerNextTime;	// 下次调度时间

 总结下来就是:XXL-job通过多线程的的方式,每次执行时:批量查询triggerNextTime为当前时间+5s之后的任务列表(最多6000个),这就是所谓的预读!

时序图

这个图上,通过时间轴的变化,人为把它划分为4种类型的任务:

  • 超期任务,调度时间在过去5s之前,这种是否触发根据任务配置的过期处理策略来执行,可能是立即补偿一次,也可能是忽略不执行
  • 过期任务,调度时间在过去5s以内,这种立即触发调度一次
  • 预读任务,调度时间在未来5s以内,这种放入任务环中,由任务环线程来执行调度
  • 未来任务,调度时间在未来5s以后,不进行处理。因为这种理论上也是没有被捞取出来的。图上为方便理解只是做个展示说明
任务触发
超期任务

对于超期任务,在前面过期处理策略已经介绍过了,这里就不再赘述,见上面

过期任务

对于过期任务,调度时间在过去5s以内,这种立即触发调度一次:

 if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、触发任务
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、更新下次执行相关时间参数
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again:// 触发一次之后,下次执行时间在未来5s以内的任务
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、获取任务环齿轮
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、放入任务环中
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } 
预读任务

预读任务,调度时间在未来5s以内,这种放入时间任务环中,由任务环线程来执行调度:

 // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、获取任务环齿轮
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、放入任务环中
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、刷新任务执行时间信息
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

所谓的时间任务环,其实就是一个内存Map:

// 时间任务环,key为时间,value为任务列表
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();

而往里面放入任务的代码如下:

private void pushTimeRing(int ringSecond, int jobId){
        // push async ring
        List<Integer> ringItemData = ringData.get(ringSecond);
        if (ringItemData == null) {
            ringItemData = new ArrayList<Integer>();
            ringData.put(ringSecond, ringItemData);
        }
        ringItemData.add(jobId);

        logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) );
    }

其中的key计算公式为:

int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

意思就是根据下次调度时间转换为秒,并与60取模。这样所有的任务计算出来,一定是在60s以内的某个值ringSecond上,这样就ringSecond为key,任务jobId放入到对应的value的集合List中了。面这就形成了XXL-job中的时间环!

时间环

从定时任务触发线程scheduleThread中通过预读生成了预读任务任务列表,如下示意图,

scheduleThread也代表里面的生产线程

而对应的消费线程就是下面要介绍的ringThread

时间环处理线程

先看下代码,还是比较直观的:

// ring thread
        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {

                while (!ringThreadToStop) {

                    // align second:线程随机休眠1s内,用以等待生产线程scheduleThread产生任务
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }

                    try {
                        // second data
                        List<Integer> ringItemData = new ArrayList<>();
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
// 从时间任务环中,取出待触发调度任务:注意这里remove取出之后,原来的就的map就没有了
                            List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                            if (tmpData != null) {
                                ringItemData.addAll(tmpData);
                            }
                        }

                        // ring trigger:立即触发任务调度
                        logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                        if (ringItemData.size() > 0) {
                            // do trigger
                            for (int jobId: ringItemData) {
                                // do trigger
                                JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                            }
                            // clear:执行完成后,清空临时列表
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
                        if (!ringThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值