Xxl-job服务端源码解析

一、启动配置类

  Xxl-job的服务端是以springboot构架为基础打包运行的,启动类为XxlJobAdminApplication,那么就先从他的配置类XxlJobAdminConfig看起。

@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {

    private static XxlJobAdminConfig adminConfig = null;
    public static XxlJobAdminConfig getAdminConfig() {
        return adminConfig;
    }

    // 调度器实例
    private XxlJobScheduler xxlJobScheduler;

    @Override
    public void afterPropertiesSet() throws Exception {
        adminConfig = this;
        // 实例化定时器
        xxlJobScheduler = new XxlJobScheduler();
        //初始化
        xxlJobScheduler.init();
    }

    // 销毁方法
    @Override
    public void destroy() throws Exception {
        xxlJobScheduler.destroy();
    }
    
    ···
}

InitializingBean、DisposableBean前面已经解释过了,我们直接看XxlJobScheduler的init方法

    public void init() throws Exception {

        // 1、国际化
        initI18n();

        // 2、监控客户端注册
        JobRegistryMonitorHelper.getInstance().start();

        // 3、执行失败的job监控
        JobFailMonitorHelper.getInstance().start();

        // 4、初始化了两个线程池,一快一慢
        JobTriggerPoolHelper.toStart();

        // 5、执行结果报表统计 定时清理xxl_job_log表的数据
        JobLogReportHelper.getInstance().start();

        // 6、启动调度线程
        JobScheduleHelper.getInstance().start();

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

1、国际化主要是通过I18nUtil加载国际化资源文件,这块实现充分利用spring的 EncodedResource和PropertiesLoaderUtils.loadProperties

2、监控客户端执行器的注册

  之前看客户端源码的时候也讲过了,每过30秒客户端执行器会发送一次注册请求,服务端接收到请求也会更新xxl_job_registry表。这里会通过查表的方式判断执行器是否存活,针对不存活的执行器,会进行剔除操作,也是每30秒执行一次。

3、执行失败的job会根据配置来触发告警、邮件等

4、初始化两个线程池,一个快速线程池,一个慢速线程池,用了执行后续的调度任务

5、报表统计,统计了例如:成功数,失败数;另外还会清理执行日志表的数据。

6、启动调度线程,关键部分

    public void start(){

        // schedule thread
        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    // 随机睡眠 4 ~ 5 秒
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }

                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                // 这里做了一个预估 每个任务的执行时间为 50ms
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

                while (!scheduleThreadToStop) {
                    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();

                        long nowTime = System.currentTimeMillis();
                        // 查询的是 下次调度时间trigger_next_time 小于 当前时间 + 5000
                        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) {

                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {

                                    // 当前时间 超出了 下次调度时间 5秒以上  忽略本次调度 计算下次调度时间
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 当前时间 超出 下次调度时间 5秒以内
                                    // 放入调度线程 等待执行
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null);

                                    // 计算下次调度时间
                                    refreshNextValidTime(jobInfo, new Date());
                                    
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
                                        // 下次的执行时间在 5s 以内
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 放入 ringData
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 修改下次的执行时间
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                                    }

                                } else {
                                    // 当前时间 小于 下次调度时间 5s 以内(因为查询时的条件时  下次调度时间 <= 当前时间 + 5)
                                    // 计算秒数
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                                    // 放入ringData 根据5秒内即将执行的任务的执行时间的秒数,将其放到timeheel对应秒数的list中
                                    pushTimeRing(ringSecond, jobInfo.getId());
                                    // 修改下次执行时间
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                                }

                            }
                            for (XxlJobInfo jobInfo: scheduleList) {
                                // 修改xxl_job_info
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }

                        } else {
                            preReadSuc = false;
                        }

                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {
                    
                        // 释放分布式锁
                        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 {
                            // 每次有任务成功加入到队列 睡眠 1s 否则 5s
                            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();

        // 执行ringData里面的任务
        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {

                // align second
                try {
                    TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!ringThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }

                while (!ringThreadToStop) {

                    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);
                            }
                            // clear
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
                        if (!ringThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }

                    // next second, align second
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }

最终会调用JobTriggerPoolHelper.addTrigger(···)方法,将调度任务放到之前创建的线程池中执行

    public void addTrigger(final int jobId, final TriggerTypeEnum triggerType, final int failRetryCount, final String executorShardingParam, final String executorParam) {

        // choose thread pool
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        // 默认情况下,会使用fastTriggerPool。
        // 如果1分钟窗口期内任务耗时达500ms超过10次,则该窗口期内判定为慢任务,慢任务自动降级进入 Slow 线程池,避免耗尽调度线程,提高系统稳定性
        if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {      // job-timeout 10 times in 1 min
            triggerPool_ = slowTriggerPool;
        }

        // trigger
        triggerPool_.execute(new Runnable() {
            @Override
            public void run() {

                long start = System.currentTimeMillis();

                try {// 执行调度
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                } finally {

                    // check timeout-count-map
                    long minTim_now = System.currentTimeMillis()/60000;
                    if (minTim != minTim_now) {
                        minTim = minTim_now;
                        jobTimeoutCountMap.clear();
                    }

                    // incr timeout-count-map
                    long cost = System.currentTimeMillis()-start;
                    if (cost > 500) {       // ob-timeout threshold 500ms
                        AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                        if (timeoutCount != null) {
                            timeoutCount.incrementAndGet();
                        }
                    }

                }

            }
        });
    }

执行调度

    public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam) {
        // load data
        XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
        if (jobInfo == null) {
            logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
            return;
        }
        if (executorParam != null) {
            jobInfo.setExecutorParam(executorParam);
        }
        int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
        // 分片
        XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());

        // sharding param
        // 组装分片参数
        int[] shardingParam = null;
        if (executorShardingParam!=null){
            String[] shardingArr = executorShardingParam.split("/");
            if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
                shardingParam = new int[2];
                shardingParam[0] = Integer.valueOf(shardingArr[0]);
                shardingParam[1] = Integer.valueOf(shardingArr[1]);
            }
        }
        // 路由策略
        if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
                && group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
                && shardingParam==null) {
            // 分片广播
            for (int i = 0; i < group.getRegistryList().size(); i++) {
                processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
            }
        } else {
            if (shardingParam == null) {
                shardingParam = new int[]{0, 1};
            }
            processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
        }

    }

通过processTrigger方法,最终调到XxlJobTrigger的runExecutor方法

    public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
        ReturnT<String> runResult = null;
        try {
            // 创建NettyHttpClient客户端
            // 通过XxlRpcReferenceBean.getObject()获取ExecutorBiz的代理对象 jdk动态代理
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
            // 调用代理对象的run方法,也就是InvocationHandler的invoke方法
            // 通过NettyHttpClient发送http请求,客户端收到请求就会执行对应的调度方法
            runResult = executorBiz.run(triggerParam);
        } catch (Exception e) {
            logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
            runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
        }

        StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
        runResultSB.append("<br>address:").append(address);
        runResultSB.append("<br>code:").append(runResult.getCode());
        runResultSB.append("<br>msg:").append(runResult.getMsg());

        runResult.setMsg(runResultSB.toString());
        return runResult;
    }

代码还是非常简单的,这里就只贴出一部分就好了,总结一下:

1、调度中心 向 执行器 发送http调度请求,执行器接收请求的服务,实际上是一台内嵌Server,默认端口9999

2、执行器执行任务逻辑,这里是异步执行

3、执行器异步使用http回调调度中心反馈调度结果,调度中心中接收回调的服务,是针对执行器开放一套API服务

二、架构图

 

 剩下还有一些Controller对应了调用中心的页面操作,这里就不分析了,代码写的还是很不错的,有参考意义。

 

posted @ 2022-03-10 14:35  上官兰夏  阅读(700)  评论(0)    收藏  举报