xxl-job 的流程详解及源码剖析

295 阅读9分钟

xxl-job 的调度中心详解:

调度中心的核心代码即为 XxlJobScheduler 类中初始化的几个线程池,下面进行详细的介绍:

XxlJobAdminConfig 类:

// 配置类属性注入完成后,执行初始化方法
@Override
public void afterPropertiesSet() throws Exception {
    adminConfig = this;

    xxlJobScheduler = new XxlJobScheduler();
    xxlJobScheduler.init();
}

XxlJobScheduler 类:

public void init() throws Exception {

    initI18n();
    
    // 开启两个线程池: fastTriggerPool 和 slowTriggerPool
    // 优先fast,如果一分钟内任务超过 10 次执行时间超过 500ms,则交给 slow 执行。
    JobTriggerPoolHelper.toStart();

    // 开启一个线程池 registryOrRemoveThreadPool
    // 开启一个守护线程用于动态维护执行器的注册表信息
    JobRegistryHelper.getInstance().start();

    // 开启一个守护线程用于失败重试
    JobFailMonitorHelper.getInstance().start();
    
    // 开启一个线程池 callbackThreadPool
    // 开启一个守护线程进行任务治理,对超时任务标记为失败
    JobCompleteHelper.getInstance().start();

    // 开启一个守护线程 logrThread,维护日志报表信息
    JobLogReportHelper.getInstance().start();
    
    // 开启两个守护线程:scheduleThread 和 ringThread
    JobScheduleHelper.getInstance().start();
}

我们详细分析一下 JobScheduleHelper 部分的代码,先通过源码分析一下 scheduleThread 做了什么:

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

        // 等待服务启动
        try {
            // 线程休眠 5s,取余操作是使线程的执行与每秒钟的整数倍对齐。
            TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
        } catch (InterruptedException e) {
            if (!scheduleThreadToStop) {
                logger.error(e.getMessage(), e);
            }
        }

        // 计算每次批量调度任务数量
        // 其中 fast + slow 默认有 300 个核心线程,每个触发器花费时间为 50 ms,则每个线程单位时间内可处理 1000 / 50 = 20 个任务
        // 批量调度任务数量默认为 300 * 20 = 6000 个
        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();
                // 查询未来 5s 内要执行的任务
                List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                if (scheduleList!=null && scheduleList.size()>0) {
                    for (XxlJobInfo jobInfo: scheduleList) {
                        if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                            // 过期时间超过 5s 的任务
                            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);
                            }
                            refreshNextValidTime(jobInfo, new Date());
                        } else if (nowTime > jobInfo.getTriggerNextTime()) {
                            // 过期时间在 5s 内的任务
                            JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                            refreshNextValidTime(jobInfo, new Date());
                            if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
                                int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                                pushTimeRing(ringSecond, jobInfo.getId());
                                refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                            }

                        } else {
                            // 还未过期并将在未来 5s 内执行的任务
                            int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                            pushTimeRing(ringSecond, jobInfo.getId());
                            refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                        }
                    }
                    // 上述代码片段详细分析看后续的流程图
                    for (XxlJobInfo jobInfo: scheduleList) {
                        XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                    }
                } else {
                    preReadSuc = false;
                }
            } catch (Exception e) {
                if (!scheduleThreadToStop) {
                    // 错误处理
                }
            } finally {
                // 提交事务,关闭资源
            }
            long cost = System.currentTimeMillis()-start;
            if (cost < 1000) {
                try {
                    // 如果执行成功,下一秒继续执行。执行失败或没查询出数据,则 5s 执行一次。
                    TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }

        }
    }
});

image.png

在分析上述代码时,未过期且在未来 5s 内将要执行的任务将会放入 ringMap 中,其中 ringThread 就是周期性的从 map 中获取任务并执行,下面通过代码详细分析:

ringThread = new Thread(new Runnable() {
    @Override
    public void run() {
        while (!ringThreadToStop) {

            try {
                // 线程休眠 1s,取余操作是使线程的执行与每秒钟的整数倍对齐。
                TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
            } catch (InterruptedException e) {
                if (!ringThreadToStop) {
                    logger.error(e.getMessage(), e);
                }
            }

            try {
                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);
                    }
                }

                if (ringItemData.size() > 0) {
                    for (int jobId: ringItemData) {
                        JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                    }
                    ringItemData.clear();
                }
            } catch (Exception e) {
                if (!ringThreadToStop) {
                    logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                }
            }
        }
    }
});

xxl-job 的执行器详解:

xxl-job 的架构比较清晰,由调度中心+执行器组成。

在之前中我们对调度中心进行了流程分析,下面我们通过源码来分析执行器,这里重点关注 jobHandlerRepository 是什么时候被插入数据的。

在执行器写入业务代码有三种方式:实现类模式,注解模式,GLUE 模式。

  • 实现类模式:继承 IJobHandler 这个抽象类,实现其中方法。然后手动注册至框架中XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler())
  • 注解模式:业务方法上添加 @XxlJob(value = "业务jobHandler名称", init = "初始化方法", destroy = "销毁方法")注解。(注解后续本质也是调用 registJobHandler)
  • GLUE 模式:业务代码维护在调度中心,支持在线 IDE 动态修改,实时编译和生效。

1、实现类模式流程分析

开发一个继承自 IJobHandler 的类 DemoJobHandler,实现其中 execute、init、destroy 方法,然后手动通过如下方式注入到执行器容器中:XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());

// 注册进 concurrentHashMap 以便后续使用
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler) {
    return jobHandlerRepository.put(name, jobHandler);
}

2、注解模式流程分析

XxlJobSpringExecutor 实现了 SmartInitializingSingleton 接口,该接口中的afterSingletonsInstantiated() 方法将会在所有的非惰性单实例 Bean 初始化完成之后进行回调。

@Override
public void afterSingletonsInstantiated() {

    initJobHandlerMethodRepository(applicationContext);
    
    GlueFactory.refreshInstance(1);
    
    try {
        super.start();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

其中 initJobHandlerMethodRepository 方法就是获取所有 Bean 中方法判断是否存在 @XxlJob 注解,如果存在则调用registJobHandler(xxlJob, bean, executeMethod)方法进行注册。

注册部分源码:

// 省略通过反射获取方法实例的代码
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));

// 注册进 concurrentHashMap 以便后续使用
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
    return jobHandlerRepository.put(name, jobHandler);
}

其中 name业务 jobHandler 名称jobHandler 包含相应的 execute()、init()destroy() 的方法实例,后续可以通过反射调用。

注解模式中 IJobHandler 抽象类的实现类为 MethodJobHandler:

@Override
public void execute() throws Exception {
    Class<?>[] paramTypes = method.getParameterTypes();
    if (paramTypes.length > 0) {
        method.invoke(target, new Object[paramTypes.length]);       // method-param can not be primitive-types
    } else {
        method.invoke(target);
    }
}

@Override
public void init() throws Exception {
    if(initMethod != null) {
        initMethod.invoke(target);
    }
}

@Override
public void destroy() throws Exception {
    if(destroyMethod != null) {
        destroyMethod.invoke(target);
    }
}

3、GLUE 模式流程分析

xxl-job 调度中心推送任务给执行器:

手动触发、cron 触发、固定周期触发最终都会交给 fast 和 slow 线程池进行任务推送。

public void addTrigger(final int jobId,
                       final TriggerTypeEnum triggerType,
                       final int failRetryCount,
                       final String executorShardingParam,
                       final String executorParam,
                       final String addressList) {
    
    // 优先fast,如果一分钟内任务超过 10 次执行时间超过 500ms,则交给 slow 执行。
    ThreadPoolExecutor triggerPool_ = fastTriggerPool;
    AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
    if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
        triggerPool_ = slowTriggerPool;
    }
    
    triggerPool_.execute(new Runnable() {
        @Override
        public void run() {

            long start = System.currentTimeMillis();

            try {
                // 进行任务推送
                XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                long minTim_now = System.currentTimeMillis()/60000;
                if (minTim != minTim_now) {
                    minTim = minTim_now;
                    jobTimeoutCountMap.clear();
                }
                long cost = System.currentTimeMillis()-start;
                if (cost > 500) {
                    AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                    if (timeoutCount != null) {
                        timeoutCount.incrementAndGet();
                    }
                }
            }
        }
    });
}

xxl-job-admin 模块,XxlJobTrigger 类:

public static void trigger(int jobId,
                           TriggerTypeEnum triggerType,
                           int failRetryCount,
                           String executorShardingParam,
                           String executorParam,
                           String addressList) {

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

    // cover addressList
    if (addressList!=null && addressList.trim().length()>0) {
        group.setAddressType(1);
        group.setAddressList(addressList.trim());
    }
    
    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]);
    }

}

路由策略如下:

  1. 第一个(FIRST):当选择该策略时,会选择执行器注册地址的第一台机器执行,如果第一台机器出现故障,则调度任务失败。
  2. 最后一个(LAST):当选择该策略时,会选择执行器注册地址的第二台机器执行,如果第二台机器出现故障,则调度任务失败。
  3. 轮询(ROUND):当选择该策略时,会按照执行器注册地址轮询分配任务,如果其中一台机器出现故障,调度任务失败,任务不会转移。
  4. 随机(RANDOM):当选择该策略时,会按照执行器注册地址随机分配任务,如果其中一台机器出现故障,调度任务失败,任务不会转移。
  5. 一致性HASH(CONSISTENT_HASH):当选择该策略时,每个任务按照Hash算法固定选择某一台机器。如果那台机器出现故障,调度任务失败,任务不会转移。
  6. 最不经常使用(LFU):当选择该策略时,会优先选择使用频率最低的那台机器,如果其中一台机器出现故障,调度任务失败,任务不会转移。
  7. 最近最久未使用(LRU):当选择该策略时,会优先选择最久未使用的机器,如果其中一台机器出现故障,调度任务失败,任务不会转移。
  8. 故障转移(FAILOVER):当选择该策略时,按照顺序依次进行心跳检测,如果其中一台机器出现故障,则会转移到下一个执行器,若心跳检测成功,会选定为目标执行器并发起调度。
  9. 忙碌转移(BUSYOVER):当选择该策略时,按照顺序依次进行空闲检测,如果其中一台机器出现故障,则会转移到下一个执行器,若空闲检测成功,会选定为目标执行器并发起调度。
  10. 分片广播(SHARDING_BROADCAST):当选择该策略时,广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务。如果其中一台机器出现故障,则该执行器执行失败,不会影响其他执行器。

通过路由策略选择机器后发送 rpc 请求至执行器(xxl-job-core,ExecutorBizClient 类):

public class ExecutorBizClient implements ExecutorBiz {

    public ExecutorBizClient() {
    }
    public ExecutorBizClient(String addressUrl, String accessToken) {
        this.addressUrl = addressUrl;
        this.accessToken = accessToken;

        // valid
        if (!this.addressUrl.endsWith("/")) {
            this.addressUrl = this.addressUrl + "/";
        }
    }

    private String addressUrl ;
    private String accessToken;
    private int timeout = 3;


    @Override
    public ReturnT<String> beat() {
        return XxlJobRemotingUtil.postBody(addressUrl+"beat", accessToken, timeout, "", String.class);
    }

    @Override
    public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam){
        return XxlJobRemotingUtil.postBody(addressUrl+"idleBeat", accessToken, timeout, idleBeatParam, String.class);
    }

    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }

    @Override
    public ReturnT<String> kill(KillParam killParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killParam, String.class);
    }

    @Override
    public ReturnT<LogResult> log(LogParam logParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logParam, LogResult.class);
    }

}

执行器这边会启动一个 netty 服务端来接受 rpc 请求,netty 服务的启动在 start 方法中:

@Override
public void afterSingletonsInstantiated() {

    initJobHandlerMethodRepository(applicationContext);
    
    GlueFactory.refreshInstance(1);
    
    try {
        super.start();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void start() throws Exception {

    // init logpath
    XxlJobFileAppender.initLogPath(logPath);

    // init invoker, admin-client
    initAdminBizList(adminAddresses, accessToken);


    // init JobLogFileCleanThread
    JobLogFileCleanThread.getInstance().start(logRetentionDays);

    // init TriggerCallbackThread
    TriggerCallbackThread.getInstance().start();

    // 初始化 netty 服务端
    initEmbedServer(address, ip, port, appname, accessToken);
}

分析一下 netty 服务端的核心代码:

@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
    String requestData = msg.content().toString(CharsetUtil.UTF_8);
    String uri = msg.uri();
    HttpMethod httpMethod = msg.method();
    boolean keepAlive = HttpUtil.isKeepAlive(msg);
    String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);

    bizThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            // 执行任务
            Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);

            String responseJson = GsonTool.toJson(responseObj);
            
            // 响应客户端
            writeResponse(ctx, keepAlive, responseJson);
        }
    });
}

private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
    if (HttpMethod.POST != httpMethod) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
    }
    if (uri == null || uri.trim().length() == 0) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
    }
    if (accessToken != null
            && accessToken.trim().length() > 0
            && !accessToken.equals(accessTokenReq)) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
    }

    // services mapping
    try {
        switch (uri) {
            case "/beat":
                return executorBiz.beat();
            case "/idleBeat":
                IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                return executorBiz.idleBeat(idleBeatParam);
            case "/run":
                TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                // 这里的 run 方法会从 jobHandlerRepository 中拿取对应方法实例进行调用
                return executorBiz.run(triggerParam);
            case "/kill":
                KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                return executorBiz.kill(killParam);
            case "/log":
                LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                return executorBiz.log(logParam);
            default:
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found.");
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
    }
}

private void writeResponse(ChannelHandlerContext ctx, boolean keepAlive, String responseJson) {
    // write response
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8));   //  Unpooled.wrappedBuffer(responseJson)
    response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");       // HttpHeaderValues.TEXT_PLAIN.toString()
    response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
    if (keepAlive) {
        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    }
    ctx.writeAndFlush(response);
}