• Xxljob服务端源码解析


    一、启动配置类

      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对应了调用中心的页面操作,这里就不分析了,代码写的还是很不错的,有参考意义。

  • 相关阅读:
    Array方面Js底层代码学习记录
    DOM 节点
    跨域
    狂雨cms代码审计:后台文件包含getshell
    在PHP一句话木马使用过程中的种种坑点分析
    记对某CMS的一次代码审计
    通达OA任意文件上传并利用文件包含导致远程代码执行漏洞分析
    DedeCMS V5.7 SP2后台存在代码执行漏洞
    zzzcms(php) v1.7.5 前台SQL注入及其他
    权限维持及后门持久化技巧总结
  • 原文地址:https://www.cnblogs.com/sglx/p/15989386.html
Copyright © 2020-2023  润新知