分布式定时任务系列7:XXL-job源码分析之任务触发
传送门分布式定时任务系列1XXL-job安装分布式定时任务系列2XXL-job使用分布式定时任务系列3任务执行引擎设计分布式定时任务系列4任务执行引擎设计续分布式定时任务系列5XXL-job中blockingQueue的应用分布式定时任务系列6XXL-job触发日志过大引发的CPU告警分布式定时任务系列7XXL-job源码分析之任务触发分布式定时任务系列8XXL-job源码分析之远程调用分布式定时任务系列9XXL-job路由策略分布式定时任务系列10XXL-job源码分析之路由策略分布式定时任务系列12XXL-job的任务触发为什么是死循环分布式定时任务系列13死循环是任务触发的银弹分布式定时任务系列14XXL-job的注册模型Java并发编程实战1java中的阻塞队列为何要看源码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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;但是理论上这个设计是有一定的隐患从时间纬度上随着程序运行周期性任务对应的触发日志数据量会不断的增大从数量纬度上业务可能会不断丰富增加许多新任务而众所周知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个 ListXxlJobInfo 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 5spass 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 5sdirect-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-readtime-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 5spass 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 5spass 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 5sdirect-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-readtime-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就是6000triggerPoolFastMax默认200triggerPoolSlowMax默认为100preReadCount (200 100) * 20 6000查询条件// 1、pre read long nowTime System.currentTimeMillis(); ListXxlJobInfo scheduleList XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime PRE_READ_MS, preReadCount);其中PRE_READ_MS代码定死了就是5s中public static final long PRE_READ_MS 5000; // pre read对应的SQLSELECT include refidBase_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 5sdirect-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-readtime-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 MapInteger, ListInteger ringData new ConcurrentHashMap();而往里面放入任务的代码如下private void pushTimeRing(int ringSecond, int jobId){ // push async ring ListInteger ringItemData ringData.get(ringSecond); if (ringItemData null) { ringItemData new ArrayListInteger(); 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 ListInteger ringItemData new ArrayList(); int nowSecond Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长跨过刻度向前校验一个刻度 for (int i 0; i 2; i) { // 从时间任务环中取出待触发调度任务注意这里remove取出之后原来的就的map就没有了 ListInteger tmpData ringData.remove( (nowSecond60-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();