绝对干货!!xxl-job任务调度源码深度解析

小强,还玩啥黑神话悟空呢,赶紧来学,绝对干货,xxl-job最核心的就是任务调度系统,里面有很多设计的精髓,建议一定要好好看下!!!
滴滴滴:友情提示:
为了让大家可以更好的理解任务调度核心源码,建议看下之前的文章:

xxl-job到底提供哪些能力?
前菜来啦!xxl-job数据表和关键字段分析
开整,开整!!xxl-job调度器源码启动流程搞起!
继续,继续!!xxl-job执行器源码启动流程!

在这里插入图片描述

1. 如何防止任务被重复调度?

xxj-job的admin每次都会调用 select * from xxl_job_lock where lock_name = ‘schedule_lock’ for update 进行加锁,加锁后才会去查之后5秒的数据,根据下次触发时间会走不同的调度规则。

在这里插入图片描述

所以假设有多个admin实例时,只有一个实例能获取锁,因此就可以避免重复调度的问题,但是这种通过for update加锁,会有以下两个问题:

1. 在锁的粒度太大,在同一时刻只有一个admin区遍历5s内待执行的任务,一旦有任务太多时,会导致整个调度的时间超过5秒,这样在进行下次调度时,会有大量的任务触发过期调用。
2. 由于for-update是加锁,假如网络或宕机导致的事务未提交,会导致后面的任务调用端拿不到对应的锁,所有任务调度会被阻塞。

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);
        //增加数据库全局锁
        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();
        //查询出接下来5s要去执行的任务
        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());

                    // 1、misfire match
                    // 查看MisfireStrategyEnum的策略,如果是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() );
                    }

                    // 2、fresh next
                    // 刷新下一次触发时间
                    refreshNextValidTime(jobInfo, new Date());

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

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

                    // 2、fresh next
                    // 刷新下次执行时间
                    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()));

                        }

                    }

                    // 3、update trigger info
                    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
            // 如果本次扫描时间小于10s
            if (cost < 1000) {
     // scan-overtime, not wait
                try {
   
                    //如果遍历5s内有执行的任务,但是花费的时间小于1s,则睡眠(1s-当前时间的换算的秒)
                    //如果遍历5s内没有执行的任务,但是睡眠(5s-当前时间的换算的秒)
                    // 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();


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

        while (!ringThreadToStop) {
   

            // align second
            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++) {
   
                    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);
                }
            }
        }

2. xxl-job的时间轮到底是如何设计的?

当调度器把接下来5秒需要执行的任务和过期的任务查询出来时,要通过pushTimeRing放到对应的时间轮,那他是如何放的呢?继续往下看!!!

xxl-job采用的是自研的时间轮,其实设计上比较简单。
底层采用的数据结构为:ConcurrentHashMapMap<Integer, List>
进入时间轮的源码:

    private void pushTimeRing(int ringSecond, int jobId){
   
        // push async ring
        // 查看当前的时间刻度是否有已有的jobId集合
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值