XXLJOB源码解析

初始化动作

客户端初始化:

客户端重要对象(可先大致看一下,在代码中碰到对应的对象时,在来看)

对象

描述

特点

JobHandler

任务执行器

具体需要调度的方法(被@Xxl标解的方法)

JobThread

任务执行线程(内部维护有队例)

每个任务都有对应的执行线程,如果在90s之内重复执行相同任务,则使用的是相同线程,反之,超过90秒未执行就会卸载工作线程JobThread (可基于实际场景优化)

ExecutorRegistryThread

执行器的心跳与注册线程

每30s运行一次调用RPC访问所有维护的服务端(可基于实际场景优化)

GlueType

运行模式,常见有Bean,Glue(Java),Glue(Shell)等等

常用一般都是bean模式(使用JobHander),不过XXL也支持直接使用脚本执行(Runtime.getRuntime().exec),本文主要讲的也是Bean模式

AdminBizClient

调用服务端对应的客户端类

一个服务器端对应一个Client,内部封装有 :

1.执行结果回调服务端

2.向服务端注册当前客户端

3.取消注册

三个接口

adminAddress

服务端地址

address

客户端地址

可以为空,为空使用本地IP加指定port

ip

客户端的地址.默认为本机

可以为空,为空则取address,或者会自动获取本机地址

port

客户端的端口,默认9999

可以为空,为空会自动在0-65535中寻找可用端口,默认9999(先寻找9999-65535区间,在寻找9998-0区间)

XxlJobLogger

配置xxlJobSpringExecutor

XxlJobSpringExecutor 实现了 SmartInitializingSingleton

里面有一个afterSingletonsInstantiated方法,在XxlJobSpringExecutor创建好bean之后会调用此方法

 

// start
@Override
public void afterSingletonsInstantiated() {

    // 初始化JobHandler ,添加了@XxlJob注解的Bean包装成JobHandler,放到Map中包括
    // private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
    //  Key就是@XxlJob所设置的 name,value就是对应的执行方法(包括init方法和destory方法)封装为一个JOBHandler
    initJobHandlerMethodRepository(applicationContext);

    // 刷新 GlueFactory,使用SpringGlueFactory实现
    // 主要用于GLUE模式执行脚本,底层使用的是Runtime.getRuntime().exec 
    GlueFactory.refreshInstance(1);

    // super start
    try { 
    // 调用XxlJobExecutor 
        super.start();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
// ---------------------- start + stop ----------------------
public void start() throws Exception {

    // init logpath  
    // 初始化日志地址(内置有默认的地址(/data/applogs/xxl-job/jobhandler/gluesource),如果自定义了地址,则地址+/gluesource)
    XxlJobFileAppender.initLogPath(logPath);

    // init invoker, admin-client, 
    // 加载服务器地址到list(adminBizList)中,服务端可以多机部署,每一个服务端则创建一个对应的AdminBizClient客户端
    initAdminBizList(adminAddresses, accessToken);


    // init JobLogFileCleanThread
    // 日志清理线程logRetentionDays 配置少于3则不进行清理
    JobLogFileCleanThread.getInstance().start(logRetentionDays);

    // init TriggerCallbackThread
    // 回调线程初始化,简单的来说就是把执行结果同步给服务端
    TriggerCallbackThread.getInstance().start();

    // init executor-server
    // 初始化RPC相关功能, 端口默认9999  与服务器建立连接
    // 同时与注册的所有服务器进行注册和心跳,每30S心跳一次
    initEmbedServer(address, ip, port, appname, accessToken);
}

服务端:

重要对象功能说明

对象

描述

特点

JobAlarm

告警接口

可自定义扩展告警信息,如钉钉告警,默认为邮件告警,维护了邮件则会触发所有的告警机制。

XxlJobAdminConfig 配置字段说明

字段

含义

默认值

备注

i18n

国服化

zh_CN

accessToken

token,与客户端 配置一致

triggerPoolFastMax

调度线程池快任务线程大小

200

最小为200

triggerPoolSlowMax

调中线程池慢任务线程大小

100

最小为100

logretentiondays

30

BEAT_TIMEOUT

每X秒进行判断为不可用状态的注册节点的移除

30s

可按需优化

DEAD_TIMEOUT

注册节点未心跳超时时间

90s

超过90s未心跳的节点将被移除,可按需优化

重要表结构说明:

1.XXL-JOB-GROUP 执行器

2.XXL-JOB-INFO(任务)

3.XXL-JOB-LOG(执行,调度日志)

4.XXL-JOB-REGISTRY(客户端心跳地址与时间)

5.XXL-JOB-LOCK(排他锁,集群部属时,可以防止重复执行。)

初始化说明:

public class XxlJobScheduler  {

public void init() throws Exception {
    // init i18n
    // 国际化初始化
    initI18n();

    // admin registry monitor run
    // 将超过90秒没有心跳的注册节点移除
    // 将正在心跳的客户端注册节点维护至xxl-job-group表中的address_list字段中(可以是多个客户端用,拼接)
    // 只针对执行器设置为自动注册的进行操作
    // 30S执行一次。
    JobRegistryMonitorHelper.getInstance().start();

    // admin fail-monitor run
    // 获取调度失败或者执行失败的日志
    // 若配置了重试次数,将进行???
    // 若配置了邮件,会发送邮件告警,可自定义
    // 10s执行一次
    JobFailMonitorHelper.getInstance().start();

    // admin lose-monitor run
    // 任务结果丢失处理
    // 调度记录停留在 "运行中"(调度成功,执行状态还是默认值0) 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
    // 60s触发一次
    JobLosedMonitorHelper.getInstance().start();

    // admin trigger pool start
    // 调度中心 
    // 创建快,慢两个调度线程(主要防止慢线程阻塞,优化速度)
    // 任务每分钟执行大于500ms超过10次将会标记为slow,执行时使用slow线程
    JobTriggerPoolHelper.toStart();

    // admin log report start
    // 报表线程,处理首页的报表相关逻辑
    JobLogReportHelper.getInstance().start();

   
    // start-schedule
    // 创建2个线程 :scheduleThread, ringThread,这个接口逻辑比前端的复杂一次,下面会补充详细代码
    // 
    // scheduleThread :
    // 使用xxl_job_lock表中的lockname=schedule_lock的数据进行分布式排他锁, 获取可处理数量(trigger中快慢两个线程池中的最大线程数相加 * qps 目前xxl计算出来qps=20)
    // 根据当前时间,查询出来在xxl_job_info表中下次执行时间小于当前时间的所有job,数量为之前获取的可处理数量。
    // 
    // ringThread:
    // 基于秒级的任务调试中心
    JobScheduleHelper.getInstance().start();

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

scheduleThread 大致执行任务逻辑

JobScheduleHelper.getInstance().start():

 

public void start(){

    // schedule thread
    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();
                    // 需要将autocommit设置为false,否则select执行之后自动提交事务
                    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();
                    // 查询下次执行时间最大为五秒内的任务(下次执行时间<=now+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) {

                            // time-ring jump
                            // 下次执行时间+5s 已经小于当前时间的不执行,直接在次计算任务的下次执行时间
                            // 执行时间+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());

                                // fresh next
                                refreshNextValidTime(jobInfo, new Date());
                            // 当前时间-5s < 执行时间 < 当前时间
                            } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                // 1、trigger
                                // 扔给调度中心进行调度,同一个job每分钟超过500ms十次之后使用slow线程池
                                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
                                // 下次执行时间,如果在5秒之内使用ring线程继续执行
                                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
                                    // 扔进ring的待执行map
                                    // map为Map<Integer,List<Integer>> 结构  key为要执行的秒,value为job ids
                                    // ringThread会从map中基于秒拿出需要执行的job
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    //基于job的下次执行时间,在刷新一下执行时间
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            } else {
                                // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time
                            // 这块跟上面差不多,这边的都是执行时间在当前时间之后的了属于未来N (N<=5) 秒之内需要执行的任务
                                // 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
                if (cost < 1000) {  // scan-overtime, not wait
                    try {
                        // pre-read period: success > scan each second; fail > skip this period;
                        // 执行时间少于1S
                        // 有调度任务:1s之内随机sleep;
                        // 无调度任务: 5s之内随机sleep; 为什么是5s之内?因为调度任务是查的当前时间+5s的
                        // preReadSuc: 是否有调度任务
                        // PRE_READ_MS:5000
                        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() {

            // 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++) {
                        //private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
                        // 取出当前秒-i的任务
                        // 循环两次,比如当前时间为59,取出58,57秒所有的任务;
                        // 其实为了避免处理时间太长,已经往前两个刻度校验了
                        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);
                    }
                }

                // 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();
}

以上,就是本文全部内容,其实XXL中还有很多思想和有趣的实现, 希望感兴趣的读者可以更深入阅读一下源码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值