xxl_job源码解析(一)任务调度和启动

前情知识

首先明确一点,当创建我们创建了一个新的任务之后,xxl_job会在数据库中的xxl_job_info表中添加一项新的记录。表中的字段如下:

CREATE TABLE `xxl_job_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_desc` varchar(255) NOT NULL,
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `author` varchar(64) DEFAULT NULL COMMENT '作者',
  `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
  `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
  `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
  `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
  `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
  `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
  `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
  `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
  `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
  `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
  `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

其中比较基本的属性有以下:

属性名意义
trigger_next_timejob的下次执行时间
trigger_last_timejob的上次执行时间
trigger_statusjob的调度状态:0-停止,1运行
misfire_strategyjob的过期调度策略

任务调度

在xxl_job服务启动时会开启很多线程,本篇中主要分析任务调度相关的jobScheduleHelper
在这里插入图片描述

在JobScheduleHelper中开辟了一个调度线程scheduleThread用于进行任务的调度,其主要流程如下:(源码中添加了相应的注

  1. 计算一个读取线程的数量,preReadCount=(快速触发器池最大线程数+慢速触发器池最大线程数)* 20。

在xxl_job中存在两个用于任务执行的线程池,分别为快速触发器池和慢速触发器池,这两个正常来说一个任务都是会在快速触发器池中进行调度执行的,但是当一个任务的执行时间过长(有设置一个阈值),则会再之后的调度中会将其放入慢速触发器池中进行。
这个预读数量为作者经过实验测试得到响应速度在1s内的合理数量,当然根据机器的性能不同响应速度会不一样。

  1. 读取数据库,取得下次执行时间trigger_next_time<=当前时间+5s的任务。

如果一直循环的读取数据库中的表,很多时候是没有返回结果的,这很蠢 这很浪费数据库资源,因此作者设计如果一次循环时间不足一秒,那么会进行休眠,如果本次读取数据库无返回值,说明未来5s内没有任务,则休眠到5s,否则休眠到1s。而为了保障任务能够在设定好的时间按时执行,将未来5s会执行的任务都读取,并放入到时间轮中,由时间轮在其正确的时间执行任务。

  1. 针对读取到的任务列表,根据其下次执行时间进行分类处理
  • 过期5s以上的情况
    根据过期策略选择对应的处理方式,DO_NOTHING:直接跳过不做处理,FIRE_ONCE_NOW:立即执行一次。
    更新任务的下一次执行时间。
  • 过期5s以内的情况
    立即执行任务一次,更新任务的下一次执行时间。
    如果新的执行时间在未来的5s内,那么就将任务信息加入时间轮之中。
  • 还未到执行时间
    将任务信息加入到时间轮之中。

除了过期策略中的任务外,其他任务真正的执行都是在时间轮中进行处理的。

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;

        while (!scheduleThreadToStop) {

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

            Connection conn = null;
            Boolean connAutoCommit = null;
            PreparedStatement preparedStatement = null;
			
            boolean preReadSuc = true;
            try {
				
                conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                connAutoCommit = conn.getAutoCommit();
                conn.setAutoCommit(false);
				// 通过数据库select ... for update会建立行级锁的机制,进行并发控制
                preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                preparedStatement.execute();

                // tx start

                // 读取时间不超过当前时间+5s的任务信息。其中PRE_READ_MS为静态变量5000
                long nowTime = System.currentTimeMillis();
                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) {

                        // 对于超时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());

                            // 获取任务信息中的过期策略,如果是FIRE_ONCE_NOW则立即执行一次
                            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() );
                            }

                            // 更新下一次执行时间
                            refreshNextValidTime(jobInfo, new Date());

                        } else if (nowTime > jobInfo.getTriggerNextTime()) {
                            // 对于过期了,但还没超过5s的任务

                            //立即执行一侧
                            JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                            logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                            // 更新下一次时间执行时间
                            refreshNextValidTime(jobInfo, new Date());

                            // 如果下一次执行时间在未来的5s内,那么就放入时间轮中
                            if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                // 计算时间轮中的位置
                                int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                // 放入时间轮中
                                pushTimeRing(ringSecond, jobInfo.getId());

                                // 再次更新下一次执行时间
                                refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                            }

                        } else {
                            //针对还未到任务执行时间的情况,将其加入时间轮之中

                            // 计算时间轮中的位置
                            int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                            // 添加入时间轮
                            pushTimeRing(ringSecond, jobInfo.getId());

                            // 计算下一次执行时间
                            refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                        }

                    }

                    // 将带有新的执行时间的任务刷新会数据库中
                    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;


            // 如果执行过快,进行休眠
            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();

任务的执行

任务的执行调度是通过一个时间轮进行的,在本项目中,时间轮是一个Map结构,总共有60个键值对,key分别为数字0到59代表秒数,value为一个任务列表,存放着在这一秒中应该执行的任务。这样只需要循环的在每一秒读取对应的任务列表,然后让其执行,就可以达到秒级的任务调度。

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

        while (!ringThreadToStop) {

            // 休眠,让每一次执行都是在整秒开始
            try {
                TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
            } catch (InterruptedException e) {
                if (!ringThreadToStop) {
                    logger.error(e.getMessage(), e);
                }
            }

            try {
                // 读取当前时间轮中的任务列表
                List<Integer> ringItemData = new ArrayList<>();
                int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                for (int i = 0; i < 2; i++) {
                    List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                    if (tmpData != null) {
                        ringItemData.addAll(tmpData);
                    }
                }
                logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                if (ringItemData.size() > 0) {
                    // 执行任务列表中的任务
                    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();
  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值