分布式任务调度系列 - XXL-JOB

一、前言

本内容仅用于个人学习笔记,如有侵扰,联系删除

二、传统的定时任务

1. 概念

1.1、定时任务的基本概念

程序为解决一个信息处理任务而预先编制的工作执行方案,这就是定时任务,核心组成如下:

  • 执行器:负责管理应用运行时环境,用于调度定时任务。
  • 任务:任务执行的流程,是一个类,具体的业务。
  • 触发器:按照某种时间规则,执行具体的调度任务。

1.2、定时任务的使用场景

日常开发中,定时任务主要分为如下两种使用场景:

  • 时间驱动:
    对账单、日结
    营销类短信
    MQ定时检查生产失败的消息
  • 数据驱动
    异步数据交换
    数据同步

2、缺点分析

  • 不支持集群多节点部署:需要自己实现避免任务重复执行的问题。
  • 不支持分片任务:处理有序数据时,多机器分片执行任务处理不同数据。
  • 不支持动态调整:不重启服务的情况下修改任务的参数。
  • 没有报警机制:当任务失败后没有报警机制通知。
  • 不支持生命周期统一管理:如不重启服务情况下关闭、启动任务。
  • 不支持失败重试:出现异常后任务终结,不能根据状态控制任务重新执行。
  • 无法统计任务数据:当任务数据量大的时候,对于任务执行情况无法高效的统计执行情况。

3、实现方式

在启动类上使用@EnableScheduling注解,表示开启定时任务

@SpringBootApplication
@EnableScheduling
public class XxlJobExecutorApplication {
	public static void main(String[] args) {
        SpringApplication.run(XxlJobExecutorApplication.class, args);
	}
}

根据业务需要,在方法上使用@Scheduled注解,cron属性配置定时规则

@Component
public class ScheduledTask {
    private final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
    @Scheduled(cron = "0/5 * * * * ?") //每5秒执行一次
    public void scheduledTaskByCorn() {
        logger.info("定时任务开始 ByCorn:" + new Date());
        scheduledTask();
        logger.info("定时任务结束 ByCorn:" + new Date());
    }
    private void scheduledTask() {
        try {
      		logger.info("调度任务正在执行:" + new Date());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

启动项目就可以看控制台看到定时任务执行效果

分布式任务调度系列 - XXL-JOB_第1张图片

三、初识xxl-job

1、概述

XXL-JOB是一个开源的,具有丰富的任务管理功能以及高性能,高可用等特点的轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展、开箱即用!!!
系统组成:

  • 调度模块(调度中心):
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
    支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
  • 执行模块(执行器):
    负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
    接收“调度中心”的执行请求、终止请求和日志请求等。

总结:

  • 执行器(分布式):天生支持任务分布式执行,无需自己实现。任务"执行器"支持集群部署,可保证任务执行;
  • 调度中心(中心式):调度中心相当于传统调度任务的触发器,调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心;

​ 调度中心和执行器两个模块分开部署,相互分离,两者之间通过RPC进行通信,其中调度中心主要是提供一个平台,管理调度信息,发送调度请求,自己不承担业务代码,而执行器接受调度中心的调度执行业务逻辑。

2、系统架构和整理流程

https://www.xuxueli.com/xxl-job/

2.1、设计思想

  • 将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
  • 将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
  • 因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;

2.2、架构图

分布式任务调度系列 - XXL-JOB_第2张图片

2.3、执行流程

分布式任务调度系列 - XXL-JOB_第3张图片

xxl_job原理概述

  1. 执行器的注册和发现
    执行器的注册和发现主要是关系两张表:

    • xxl_job_group:每个服务注册的实例列表。
    • xxl_job_registry:执行器的实例表,保存实例信息和心跳信息。

    执行器启动线程每隔30秒向注册表xxl_job_registry请求一次,更新执行器的心跳信息,调度中心启动 线程每隔30秒检测一次xxl_job_registry,将超过90秒还没有收到心跳的实例信息从xxl_job_registry删除,并更新xxl_job_group服务的实例列表信息。

  2. 调度中心调用执行器

    调度中心的操作:

    调度中心通过循环不停的:

    1. 关闭自动提交事务

    2. 利用mysql的悲观锁,其他事务无法进入

      select * from xxl_job_lock where lock_name = 'schedule_lock' for update
      
  3. 读取数据库中的xxl_job_info:记录定时任务的相关信息,该表中有trigger_next_time字段表示下一次任务的触发时间。拿到距离当前时间5s内的任务列表,分为三种情况处理:

    • 对于当前时间-任务的下一次触发时间>5,直接跳过不执行,重置trigger_next_time的时间。(超过5s)

    • 对于任务的下一次触发时间<当前时间<任务的下一次触发时间+5的任务(不超过5s的):

      1. 开线程处理执行触发逻辑,根据当前时间更新下一次任务触发时间

      2. 如果新的任务下一次触发时间-当前时间<5,放到时间轮中,时间轮是一个map:

       private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
      
      1. 根据新的任务下一次触发时间更新下下一次任务触发时间
    • 对于任务的下一次触发时间>当前时间,将其放入时间轮中,根据任务下一次触发时间更新下下一次任务触发时间

  4. commit提交事务,同时释放排他锁

  5. 执行器的操作:

    1. 执行器接收到调度中心的调度信息,将调度信息放到对应的任务的等待队列中
    2. 执行器的任务处理线程从任务队列中取出调度信息,执行业务逻辑,将结果放入一个公共的等待队列中(每个任务都有一个单独的处理线程和等待队列,任务信息放入该队列中)
    3. 执行器有一个专门的回调线程定时批量从结果队列中取出任务结果,并且回调告知调度中心

四 xxl-job实战

  • 文档地址:https://www.xuxueli.com/xxl-job/
  • 源码仓库地址
源码仓库地址 Release Download
github地址 https://github.com/xuxueli/xxl-job
gitee地址 https://github.com/xuxueli/xxl-job
  • 中央仓库地址
<dependency>
    <groupId>com.xuxueligroupId>
    <artifactId>xxl-job-coreartifactId>
    <version>${最新稳定版本}version>
dependency>
  • 环境:

    Maven3+
    Jdk1.8+
    Mysql5.7+

  • 项目模块
    分布式任务调度系列 - XXL-JOB_第4张图片
    可分为有四大模块

  • doc:存放相关文档

  • xxl-job-admin:调度中心管理后台

  • xxl-job-core:框架核心包

  • xxl-job-executor-samples:集成不同执行器的案例代码
    注:本次实战使用的是xxl-job2.3.1版本

1、构建本地数据库

分布式任务调度系列 - XXL-JOB_第5张图片
可在这个目录下找到对应sql,并在本地执行创建项目所需要的数据库
分布式任务调度系列 - XXL-JOB_第6张图片

2、修改adminapplication.properties并启动管理后台

调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties

配置文件修改数据库连接

  • spring.datasource.url
  • spring.datasource.username
  • spring.datasource.password

配置文件的配置及说明:

### 调度中心JDBC链接
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
[email protected]
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN
## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30

启动后,访问http://localhost:8080/xxl-job-admin;默认登录账户名:admin。登录密码:123456
分布式任务调度系列 - XXL-JOB_第7张图片
这是登录后先来到运行列表界面,这里以图形化来展示任务整体的执行情况
分布式任务调度系列 - XXL-JOB_第8张图片
这是查看调度日志,根据日志来查看任务具体的执行情况
分布式任务调度系列 - XXL-JOB_第9张图片

这是配置执行器,这边选择自动注册就行,等执行器启动的时候会被调度中心监听到并加入地址列表
分布式任务调度系列 - XXL-JOB_第10张图片

这是配置任务执行任务的,以新配置的测试任务2为例
分布式任务调度系列 - XXL-JOB_第11张图片

路由策略:多用于集群时的配置。这里属于本机测试,所以选第一个
Cron:执行规则,这里定的是两秒执行一次
JobHandler:定义执行器的名字,这里使用的是MyJobHandler

3、编写执行器代码并测试定时任务

这里以xxl-job-executor-sample-springboot为例:
修改配制文件
xxl.job.admin.addresses:为调度中心的地址
xxl.job.executor.appname:这里对应的是执行器的AppName
xxl.job.executor.port:这里为RPC的监听端口

### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-demo
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

编写执行任务,有两种方式

  • 方式一:Bean模式(在方法上使用@XxlJob注解定义执行器)
@Component
public class XxlJobDemoHandler {
    /**
     * Bean模式,一个方法为一个任务
     * 1、在Spring Bean实例中,开发Job方法,方式格式要求为 "public ReturnT execute(String param)"
     * 2、为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
     * 3、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志;
     */
    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        System.out.println("===============demoJobHandler==================");
        XxlJobHelper.log("XXL-JOB, Hello World.");
        System.out.println("简单任务示例(Bean模式)");
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }
}

分布式任务调度系列 - XXL-JOB_第12张图片

  • 方式二:继承IJobHandler的模式(并且在类上使用@JobHandler注解定义执行器)

    @Component
    public class DemoXxlJob extends IJobHandler implements InitializingBean {
    
      @Override
      public void execute() throws Exception {
          SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
           System.out.println("当前时间是:" + format.format(new Date()));
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
    
            System.out.println("创建对象");
            XxlJobExecutor.registJobHandler("MyXxlJob", this);
    
       }
       @Override
       public void init() throws Exception {
           System.out.println("开始初始化了............");
        }
    
    
        /**
         * destroy handler, invoked when JobThread destroy
         */
        @Override
        public void destroy() throws Exception {
            System.out.println("开始结束了............");
        }
    
    }
    

在调度中心添加执行器实例并配置机器地址
分布式任务调度系列 - XXL-JOB_第13张图片
启动测试任务2
分布式任务调度系列 - XXL-JOB_第14张图片

【结果】
分布式任务调度系列 - XXL-JOB_第15张图片

五、源码分析

1、启动流程

1.1、服务器启动流程

分布式任务调度系列 - XXL-JOB_第16张图片
com.xxl.job.admin.core.conf.XxlJobAdminConfig

首先找到配置类 XxlJobAdminConfig,
分布式任务调度系列 - XXL-JOB_第17张图片
可以发现该类实现 InitializingBean接口,这里直接看 afterPropertiesSet方法即可。

com.xxl.job.admin.core.conf.XxlJobAdminConfig#afterPropertiesSet()

    @Override
    public void afterPropertiesSet() throws Exception {
        //利用静态声明的只会加载一次的特性,初始化一个单例对象。
        adminConfig = this;

        //初始化xxjob调度器
        xxlJobScheduler = new XxlJobScheduler();
        xxlJobScheduler.init();
    }

初始化xxjob调度器

com.xxl.job.admin.core.scheduler.XxlJobScheduler#init()

    public void init() throws Exception {
        // init i18n
        initI18n();

        // admin trigger pool start
        // 初始化触发器线程池
        JobTriggerPoolHelper.toStart();

        // admin registry monitor run
        /**
         * 30秒执行一次,维护注册表信息, 判断在线超时时间90s
         *    1. 删除90s未有心跳的执行器节点;xxl_job_registry
         *    2. 获取所有的注册节点,更新到xxl_job_group(执行器)
         */
        JobRegistryHelper.getInstance().start();

        // admin fail-monitor run
        // 运行失败监视器,主要失败发送邮箱,重试触发器
        JobFailMonitorHelper.getInstance().start();

        // admin lose-monitor run ( depend on JobTriggerPoolHelper )
        // 将丢失主机调度日志置为失败
        JobCompleteHelper.getInstance().start();

        // admin log report start
        // 统计一些失败成功报表
        JobLogReportHelper.getInstance().start();

        // start-schedule  ( depend on JobTriggerPoolHelper )
        /**
         * 调度器执行任务(两个线程 + 线程池执行调度逻辑)
         *      1. 调度线程50s执行一次;查询5s秒内执行的任务,并按照不同逻辑执行
         *      2. 时间轮线程每1秒执行一次;时间轮算法,并向前跨一个时刻;
         */
        JobScheduleHelper.getInstance().start();

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

该方法主要做了如下事情:

  • init i18n
  • 初始化触发器线程池
  • 维护注册表信息(30秒执行一次)
  • 将丢失主机信息调度日志更改状态
  • 统计一些失败成功报表,删除过期日志
  • 执行调度器

1.1.1、初始化触发器线程池

com.xxl.job.admin.core.thread.JobTriggerPoolHelper#toStart()

    public static void toStart() {
        helper.start();
    }
 public void start(){
        //最大200线程,最多处理1000任务
        fastTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(1000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode());
                    }
                });

        //最大100线程,最多处理2000任务
        //一分钟内超时10次,则采用慢触发器执行
        slowTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(2000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode());
                    }
                });
    }

注意:这里分别初始化了2个线程池,一个快一个慢,优先选择快,当一分钟以内任务超过10次执行时间超过500ms,则加入慢线程池执行。

1.1.2、维护注册表信息(30秒执行一次)

这里分别做了三件事

  1. 初始化注册或者删除线程池,主要负责客户端注册或者销毁到xxl_job_registry表
  2. 剔除超时注册机器
  3. 更新xxl_job_group执行器地址列表
	public void start(){

		// for registry or remove
		//初始化注册或者删除线程池
		registryOrRemoveThreadPool = new ThreadPoolExecutor(
				2,
				10,
				30L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(2000),
				new ThreadFactory() {
					@Override
					public Thread newThread(Runnable r) {
						return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
					}
				},
				//注意:这里的拒绝策略就是再次执行...
				new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						r.run();
						logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
					}
				});

		// for monitor 30秒执行一次,维护注册表信息, 判断在线超时时间90s
		registryMonitorThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (!toStop) {
					try {
						// 查询xxl_job_group执行器地址类型为0-自动注册的注册信息
						List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
						if (groupList!=null && !groupList.isEmpty()) {

							// remove dead address (admin/executor)
							//  从xxl_job_registry注册表中删除超时的机器
							List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (ids!=null && ids.size()>0) {
								//  删除90s未有心跳的执行器节点;xxl_job_registry
								XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
							}

							// fresh online address (admin/executor)
							// 获取所有在线机器
							HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
							List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (list != null) {
								for (XxlJobRegistry item: list) {
									// 将注册类型为EXECUTOR的XxlJobRegistry集合改装成appname=>设置触发器的ip地址
									if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
										String appname = item.getRegistryKey();
										List<String> registryList = appAddressMap.get(appname);
										if (registryList == null) {
											registryList = new ArrayList<String>();
										}

										if (!registryList.contains(item.getRegistryValue())) {
											registryList.add(item.getRegistryValue());
										}
										appAddressMap.put(appname, registryList);
									}
								}
							}

							// fresh group address
							// 获取所有的注册节点,更新到xxl_job_group(执行器)
							for (XxlJobGroup group: groupList) {
								List<String> registryList = appAddressMap.get(group.getAppname());
								String addressListStr = null;
								if (registryList!=null && !registryList.isEmpty()) {
									Collections.sort(registryList);
									StringBuilder addressListSB = new StringBuilder();
									for (String item:registryList) {
										addressListSB.append(item).append(",");
									}
									addressListStr = addressListSB.toString();
									addressListStr = addressListStr.substring(0, addressListStr.length()-1);
								}
								group.setAddressList(addressListStr);
								group.setUpdateTime(new Date());

								XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
							}
						}
					} catch (Exception e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
					try {
						TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
					} catch (InterruptedException e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
				}
				logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
			}
		});
		registryMonitorThread.setDaemon(true);
		registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
		registryMonitorThread.start();
	}

1.1.3、运行失败监视器,主要失败发送邮箱,重试触发器

这里分别做了三件事

  1. 失败重试
    这里判断失败有2种情况(trigger_code表示调度中心调用执行器状态,handle_code表示执行器执行结束后回调给调度中心的状态,200均标识成功,500标识失败)
    第一种:trigger_code!=(0,200) 且 handle_code!=0
    第二种:handle_code!=200
  2. 告警(这里可向spring注入JobAlarm)

com.xxl.job.admin.core.thread.JobFailMonitorHelper#start()

	public void start(){
		monitorThread = new Thread(new Runnable() {

			@Override
			public void run() {

				// monitor
				while (!toStop) {
					try {
						// 获取执行失败的日志 调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
						List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
						// 1:执行触发器成功,返回值失败.2:触发器失败
						if (failLogIds!=null && !failLogIds.isEmpty()) {
							for (long failLogId: failLogIds) {

								// lock log   加锁,乐观修改alarm_status=-1
								int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
								if (lockRet < 1) {
									continue;
								}
								XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
								XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());

								// 1、fail retry monitor
								if (log.getExecutorFailRetryCount() > 0) {  // 若可重试次数 > 0, 则再次执行触发器
									JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
									String retryMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
"
; log.setTriggerMsg(log.getTriggerMsg() + retryMsg); XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); } // 2、fail alarm monitor 失败警告监视器 int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 if (info != null) { boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log); newAlarmStatus = alarmResult?2:3; // 获取警报执行状态 } else { // 没设置报警邮箱,则更改状态为不需要告警 newAlarmStatus = 1; } // 释放锁 XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus); } } } catch (Exception e) { if (!toStop) { logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); } } try { TimeUnit.SECONDS.sleep(10); } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop"); } }); monitorThread.setDaemon(true); monitorThread.setName("xxl-job, admin JobFailMonitorHelper"); monitorThread.start(); }

在这里可通过实现JobAlarm接口,注入spring容器执行报警。

boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);

com.xxl.job.admin.core.alarm.JobAlarmer#alarm()

    private ApplicationContext applicationContext;
    private List<JobAlarm> jobAlarmList;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class);
        if (serviceBeanMap != null && serviceBeanMap.size() > 0) {
            jobAlarmList = new ArrayList<JobAlarm>(serviceBeanMap.values());
        }
    }

    /**
     * job alarm
     *
     * @param info
     * @param jobLog
     * @return
     */
    public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) {

        boolean result = false;
        if (jobAlarmList!=null && jobAlarmList.size()>0) {
            result = true;  // success means all-success
            for (JobAlarm alarm: jobAlarmList) {
                boolean resultItem = false;
                try {
                    resultItem = alarm.doAlarm(info, jobLog);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
                if (!resultItem) {
                    result = false;
                }
            }
        }

        return result;
    }

这里jobAlarmList集合里面有很多JobAlarm对象, 都是执行初始化方法从spring注入的,也就是说扩展的话,只需要实现JobAlarm接口,注入spring即可。

1.1.4、将丢失主机信息调度日志更改状态

public void start(){

		// for callback
		callbackThreadPool = new ThreadPoolExecutor(
				2,
				20,
				30L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(3000),
				new ThreadFactory() {
					@Override
					public Thread newThread(Runnable r) {
						return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode());
					}
				},
				new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						r.run();
						logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now).");
					}
				});


		// for monitor
		monitorThread = new Thread(new Runnable() {

			@Override
			public void run() {

				// wait for JobTriggerPoolHelper-init
				try {
					TimeUnit.MILLISECONDS.sleep(50);
				} catch (InterruptedException e) {
					if (!toStop) {
						logger.error(e.getMessage(), e);
					}
				}

				// monitor
				while (!toStop) {
					try {
						// 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
						Date losedTime = DateUtil.addMinutes(new Date(), -10);
						// 调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等
						List<Long> losedJobIds  = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime);

						if (losedJobIds!=null && losedJobIds.size()>0) {
							for (Long logId: losedJobIds) {

								XxlJobLog jobLog = new XxlJobLog();
								jobLog.setId(logId);

								jobLog.setHandleTime(new Date());
								jobLog.setHandleCode(ReturnT.FAIL_CODE);
								jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") );
								// 更改处理状态
								XxlJobCompleter.updateHandleInfoAndFinish(jobLog);
							}

						}
					} catch (Exception e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e);
						}
					}

                    try {
                        TimeUnit.SECONDS.sleep(60);
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }

                }

				logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop");

			}
		});
		monitorThread.setDaemon(true);
		monitorThread.setName("xxl-job, admin JobLosedMonitorHelper");
		monitorThread.start();
	}

更改处理状态

	public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) {

        // finish 若父任务正常结束,则终止子任务,以及设置Childmsg
        finishJob(xxlJobLog);

        // text最大64kb 避免长度过长 截断超过长度限制字符
        if (xxlJobLog.getHandleMsg().length() > 15000) {
            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) );
        }

        // fresh handle 更新超时joblog
        return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog);
    }

1.1.5、统计一些失败成功报表,删除过期日志

    public void start(){
        logrThread = new Thread(new Runnable() {
            // 每分钟刷新一次
            @Override
            public void run() {

                // last clean log time   记录上次清除日志时间
                long lastCleanLogTime = 0;


                while (!toStop) {

                    // 1、log-report refresh: refresh log report in 3 days
                    try {

                        for (int i = 0; i < 3; i++) {

                            // today  分别统计今天,昨天,前天0~24点的数据
                            Calendar itemDay = Calendar.getInstance();
                            itemDay.add(Calendar.DAY_OF_MONTH, -i);
                            itemDay.set(Calendar.HOUR_OF_DAY, 0);
                            itemDay.set(Calendar.MINUTE, 0);
                            itemDay.set(Calendar.SECOND, 0);
                            itemDay.set(Calendar.MILLISECOND, 0);

                            Date todayFrom = itemDay.getTime();

                            itemDay.set(Calendar.HOUR_OF_DAY, 23);
                            itemDay.set(Calendar.MINUTE, 59);
                            itemDay.set(Calendar.SECOND, 59);
                            itemDay.set(Calendar.MILLISECOND, 999);

                            Date todayTo = itemDay.getTime();

                            // refresh log-report every minute
                            //设置默认值
                            XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
                            xxlJobLogReport.setTriggerDay(todayFrom);
                            xxlJobLogReport.setRunningCount(0);
                            xxlJobLogReport.setSucCount(0);
                            xxlJobLogReport.setFailCount(0);

                            //查询失败, 成功,总的调用次数
                            Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo);
                            if (triggerCountMap!=null && triggerCountMap.size()>0) {
                                int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0;
                                int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0;
                                int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0;
                                int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;

                                xxlJobLogReport.setRunningCount(triggerDayCountRunning);
                                xxlJobLogReport.setSucCount(triggerDayCountSuc);
                                xxlJobLogReport.setFailCount(triggerDayCountFail);
                            }

                            // 刷新调用次数,若找不到则默认都是0
                            int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport);
                            if (ret < 1) {
                                //没数据则保存
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport);
                            }
                        }

                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e);
                        }
                    }

                    // 设置了保留日志天数且日志保留了24小时,则进入
                    if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0
                            && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) {

                        // 通过日志保留天数算出清除log时间
                        Calendar expiredDay = Calendar.getInstance();
                        expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays());
                        expiredDay.set(Calendar.HOUR_OF_DAY, 0);
                        expiredDay.set(Calendar.MINUTE, 0);
                        expiredDay.set(Calendar.SECOND, 0);
                        expiredDay.set(Calendar.MILLISECOND, 0);
                        Date clearBeforeTime = expiredDay.getTime();

                        // clean expired log
                        List<Long> logIds = null;
                        do {
                            // 这里传了3个0表示查询所有,而不是单个任务id
                            logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
                            // 删除过期数据
                            if (logIds!=null && logIds.size()>0) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds);
                            }
                        } while (logIds!=null && logIds.size()>0);

                        // update clean time
                        lastCleanLogTime = System.currentTimeMillis();
                    }

                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, job log report thread stop");

            }
        });
        logrThread.setDaemon(true);
        logrThread.setName("xxl-job, admin JobLogReportHelper");
        logrThread.start();
    }

1.1.6、执行调度器

分布式任务调度系列 - XXL-JOB_第18张图片
map这里用到了时间轮,xxl-job 为了避免处理耗时太长,会跨过刻度,多向前校验一个刻度;也就是当指针指到 2s 时,会把 1s 和 2s 位置的任务同时读取出来。
分布式任务调度系列 - XXL-JOB_第19张图片

    public void start(){

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

                try {
                    // 保证5秒执行一次
                    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.");
                // 每秒处理20个任务,200个线程
                // 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();
                        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();
                        // 获取当前时间后5秒,同时最多负载的分页数
                        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(时间轮)
                                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 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
                                    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
                                    // 未来五秒以内执行的所有任务添加到ringData
                                    // 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;
                            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) {
                                // 执行触发器;逻辑就跟主动触发是一致的了。
                                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);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }

1.2、客户端启动流程

构建作业执行器

这里将 XxlJobSpringExecutor 交由容器托管

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        // xxjob管理地址
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        // 注册应用名称
        xxlJobSpringExecutor.setAppname(appname);
        // xxl作业执行器注册表地址
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        // 注册地址的token
        xxlJobSpringExecutor.setAccessToken(accessToken);
        // 日志路径
        xxlJobSpringExecutor.setLogPath(logPath);
        // 日志保存天数
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

老规矩,直接看接口继承体系,可以发现注入了 ApplicationContext 对象。以及实现了 SmartInitializingSingleton 接口,实现该接口的当spring容器初始完成,紧接着执行监听器发送监听后,就会遍历所有的Bean然后初始化所有单例非懒加载的bean,最后在实例化阶段结束时触发回调接口。
分布式任务调度系列 - XXL-JOB_第20张图片
分布式任务调度系列 - XXL-JOB_第21张图片
这里我们看XxlJobSpringExecutor,实现了 SmartInitializingSingleton 接口,实现该接口的当spring容器初始完成,调用afterSingletonsInstantiated()方法。紧接着执行监听器发送监听后,就会遍历所有的Bean然后初始化所有单例非懒加载的bean。实现DisposableBean当实例bean摧毁时调用destroy()方法。

public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobSpringExecutor.class);


    // start
    @Override
    public void afterSingletonsInstantiated() {

        // init JobHandler Repository
        /*initJobHandlerRepository(applicationContext);*/

        // init JobHandler Repository (for method)
        // init JobHandler Repository (for method)
        /**
         * 初始化调度器资源管理器
         * ConcurrentMap jobHandlerRepository = new ConcurrentHashMap();
         * handle名; Handler->MethodJobHandler(反射 Object、Bean、initMethod、destroyMethod)
         */
        initJobHandlerMethodRepository(applicationContext);

        // refresh GlueFactory 刷新GlueFactory
        GlueFactory.refreshInstance(1);

        // super start
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

该方法主要做了如下事情:

  1. 初始化调度器资源管理器

1.2.1、初始化调度器资源管理器

该方法主要做了如下事情:

  1. spring容器获取所有对象,并遍历查找方法上标记XxlJob注解的方法
  2. xxljob配置的jobname作为key,对象,反射的执行,初始,销毁方法作为value注册jobHandlerRepository

com.xxl.job.core.executor.impl.XxlJobSpringExecutor#initJobHandlerMethodRepositor()

private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
        if (applicationContext == null) {
            return;
        }
        // init job handler from method
        String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
        // 遍历每个容器对象
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = applicationContext.getBean(beanDefinitionName);

            Map<Method, XxlJob> annotatedMethods = null;   // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
            try {
                // 获取每个注解XxlJob方法
                annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                        new MethodIntrospector.MetadataLookup<XxlJob>() {
                            @Override
                            public XxlJob inspect(Method method) {
                                return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                            }
                        });
            } catch (Throwable ex) {
                logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
            }
            if (annotatedMethods==null || annotatedMethods.isEmpty()) {
                continue;
            }
            //遍历标记了XxlJob注解的方法
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                Method executeMethod = methodXxlJobEntry.getKey();
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                // regist
                registJobHandler(xxlJob, bean, executeMethod);
            }
        }
    }

com.xxl.job.core.executor.XxlJobExecutor#registJobHandler()

protected void registJobHandler(XxlJob xxlJob, Object bean, Method executeMethod){
        if (xxlJob == null) {
            return;
        }
        // 获取配置xxjob的触发器名称
        String name = xxlJob.value();
        //make and simplify the variables since they'll be called several times later
        Class<?> clazz = bean.getClass();
        String methodName = executeMethod.getName();
        if (name.trim().length() == 0) {
            throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + clazz + "#" + methodName + "] .");
        }
        // 工作处理资源库是否有相同命名
        if (loadJobHandler(name) != null) {
            throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
        }

        // execute method
        /*if (!(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(String.class))) {
            throw new RuntimeException("xxl-job method-jobhandler param-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
                    "The correct method format like \" public ReturnT execute(String param) \" .");
        }
        if (!method.getReturnType().isAssignableFrom(ReturnT.class)) {
            throw new RuntimeException("xxl-job method-jobhandler return-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
                    "The correct method format like \" public ReturnT execute(String param) \" .");
        }*/
        // 设置可访问,设置后可通过反射调用私有方法
        executeMethod.setAccessible(true);

        // init and destroy
        Method initMethod = null;
        Method destroyMethod = null;

        if (xxlJob.init().trim().length() > 0) {
            try {
                // 获取XxlJob标记的方法,配置的init方法
                initMethod = clazz.getDeclaredMethod(xxlJob.init());
                initMethod.setAccessible(true);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] .");
            }
        }
        if (xxlJob.destroy().trim().length() > 0) {
            try {
                // 获取XxlJob标记的方法,配置的destroy方法
                destroyMethod = clazz.getDeclaredMethod(xxlJob.destroy());
                destroyMethod.setAccessible(true);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + clazz + "#" + methodName + "] .");
            }
        }

        // registry jobhandler 将xxljob配置的jobname作为key,对象,反射的执行,初始,销毁方法作为value注册jobHandlerRepository中
        registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));

    }

再看super.start()

 public void start() throws Exception {

        // init logpath 初始化日志目录,用来存储调度日志执行指令到磁盘
        XxlJobFileAppender.initLogPath(logPath);

        // init invoker, admin-client 初始化admin链接路径存储集合
        // 在AdminBizClient设置好addressUrl+accessToken
        initAdminBizList(adminAddresses, accessToken);


        // init JobLogFileCleanThread 清除过期日志(30天)
        // 根据存储路径目录的日志(目录名为时间),根据其目录时间进行删除,1天跑一次,守护线程
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // init TriggerCallbackThread 回调调度中心任务执行状态
        TriggerCallbackThread.getInstance().start();

        // init executor-server  执行内嵌服务
        /**
         * 1. 使用netty开放端口,等待服务端调用
         * 2. 维护心跳时间到服务端(心跳30S)
         * 3. 向服务端申请剔除服务
         */
        initEmbedServer(address, ip, port, appname, accessToken);
    }

1.2.2、刷新GlueFactory

com.xxl.job.core.glue.GlueFactory#refreshInstance()

	public static void refreshInstance(int type){
		if (type == 0) {
			glueFactory = new GlueFactory();
		} else if (type == 1) {
			glueFactory = new SpringGlueFactory();
		}
	}

1.2.3、核心启动类

该方法主要做了如下事情:

  1. 初始化日志文件
  2. 初始化admin链接路径存储集合
  3. 清除过期日志
  4. 回调调度中心任务执行状态
  5. 执行内嵌服务

com.xxl.job.core.executor.XxlJobExecutor#start()

public void start() throws Exception {

        // init logpath
        // init logpath 初始化日志目录,用来存储调度日志执行指令到磁盘
        XxlJobFileAppender.initLogPath(logPath);

        // init invoker, admin-client 初始化admin链接路径存储集合
        // 在AdminBizClient设置好addressUrl+accessToken
        initAdminBizList(adminAddresses, accessToken);


        // init JobLogFileCleanThread 清除过期日志(30天)
        // 根据存储路径目录的日志(目录名为时间),根据其目录时间进行删除,1天跑一次,守护线程
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // init TriggerCallbackThread 回调调度中心任务执行状态
        TriggerCallbackThread.getInstance().start();

        // init executor-server  执行内嵌服务
        /**
         * 1. 使用netty开放端口,等待服务端调用
         * 2. 维护心跳时间到服务端(心跳30S)
         * 3. 向服务端申请剔除服务
         */
        initEmbedServer(address, ip, port, appname, accessToken);
    }
  1. 初始化日志文件

com.xxl.job.core.log.XxlJobFileAppender#initLogPath()

	public static void initLogPath(String logPath){
		// init
		if (logPath!=null && logPath.trim().length()>0) {
			logBasePath = logPath;
		}
		// mk base dir 日志文件不存在则创建
		File logPathDir = new File(logBasePath);
		if (!logPathDir.exists()) {
			logPathDir.mkdirs();
		}
		logBasePath = logPathDir.getPath();

		// mk glue dir 创建glue目录
		File glueBaseDir = new File(logPathDir, "gluesource");
		if (!glueBaseDir.exists()) {
			glueBaseDir.mkdirs();
		}
		glueSrcPath = glueBaseDir.getPath();
	}
  1. 初始化admin链接路径存储集合
    private void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
        if (adminAddresses!=null && adminAddresses.trim().length()>0) {
            for (String address: adminAddresses.trim().split(",")) {
                // 多个admin地址以,分隔
                if (address!=null && address.trim().length()>0) {

                    AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken);

                    if (adminBizList == null) {
                        adminBizList = new ArrayList<AdminBiz>();
                    }
                    // 将admin地址以及token添加adminBiz中
                    adminBizList.add(adminBiz);
                }
            }
        }
    }
  1. 主要清除过期日志
public void start(final long logRetentionDays){

        // limit min value 日志最大保存天数<3天,直接退出
        if (logRetentionDays < 3 ) {
            return;
        }
        // 一天执行一次
        localThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!toStop) {
                    try {
                        // clean log dir, over logRetentionDays 查询目录下所有子文件(包含目录)
                        File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles();
                        if (childDirs!=null && childDirs.length>0) {

                            // today   获取今天0点时间
                            Calendar todayCal = Calendar.getInstance();
                            todayCal.set(Calendar.HOUR_OF_DAY,0);
                            todayCal.set(Calendar.MINUTE,0);
                            todayCal.set(Calendar.SECOND,0);
                            todayCal.set(Calendar.MILLISECOND,0);

                            Date todayDate = todayCal.getTime();

                            for (File childFile: childDirs) {

                                // valid  不是目录跳过
                                if (!childFile.isDirectory()) {
                                    continue;
                                }
                                //查询不到'-'则跳过
                                if (childFile.getName().indexOf("-") == -1) {
                                    continue;
                                }

                                // file create date  获取文件创建时间,文件都是以年-月-日命名的
                                Date logFileCreateDate = null;
                                try {
                                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                                    logFileCreateDate = simpleDateFormat.parse(childFile.getName());
                                } catch (ParseException e) {
                                    logger.error(e.getMessage(), e);
                                }
                                if (logFileCreateDate == null) {
                                    continue;
                                }
                                // 大于日志最大存活时间则清除
                                if ((todayDate.getTime()-logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000) ) {
                                    FileUtil.deleteRecursively(childFile);
                                }

                            }
                        }

                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
                        // 睡眠一天处理
                        TimeUnit.DAYS.sleep(1);
                    } catch (InterruptedException e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destroy.");

            }
        });
        localThread.setDaemon(true);
        localThread.setName("xxl-job, executor JobLogFileCleanThread");
        localThread.start();
    }
  1. 回调调度中心任务执行状态
public void start() {

        // valid 是否有配置admin路径
        if (XxlJobExecutor.getAdminBizList() == null) {
            logger.warn(">>>>>>>>>>> xxl-job, executor callback config fail, adminAddresses is null.");
            return;
        }

        // callback
        triggerCallbackThread = new Thread(new Runnable() {

            @Override
            public void run() {

                // normal callback
                while(!toStop){
                    try {
                        //获取回调参数
                        HandleCallbackParam callback = getInstance().callBackQueue.take();
                        if (callback != null) {

                            // callback list param
                            List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                            // 移除队列中所有元素到callbackParamList中
                            int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                            callbackParamList.add(callback);

                            // callback, will retry if error 通知admin
                            if (callbackParamList!=null && callbackParamList.size()>0) {
                                doCallback(callbackParamList);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }

                // last callback
                try {
                    List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                    int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                    if (callbackParamList!=null && callbackParamList.size()>0) {
                        doCallback(callbackParamList);
                    }
                } catch (Exception e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor callback thread destroy.");

            }
        });
        triggerCallbackThread.setDaemon(true);
        triggerCallbackThread.setName("xxl-job, executor TriggerCallbackThread");
        triggerCallbackThread.start();

  1. 执行内嵌服务

该方法主要做了如下事情:

  1. 使用netty开放端口,等待服务端调用
  2. 注册到服务端(心跳30S)
  3. 向服务端申请剔除服务

com.xxl.job.core.executor.XxlJobExecutor#initEmbedServer()

private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {

        // fill ip port  若没设置端口,则寻找可用端口
        port = port>0?port: NetUtil.findAvailablePort(9999);
        // 若没设置IP,则获取本机Ip
        ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();

        // generate address 构造地址,若没设置地址,则将ip,port拼接成地址
        if (address==null || address.trim().length()==0) {
            String ip_port_address = IpUtil.getIpPort(ip, port);   // registry-address:default use address to registry , otherwise use ip:port if address is null
            address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
        }

        // accessToken
        if (accessToken==null || accessToken.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
        }

        // start  启动嵌入服务器 ,向服务端注册,以及监听端口,主要服务服务端调用。
        embedServer = new EmbedServer();
        embedServer.start(address, port, appname, accessToken);
    }

com.xxl.job.core.server.EmbedServer#start()

    public void start(final String address, final int port, final String appname, final String accessToken) {
        executorBiz = new ExecutorBizImpl();
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // param
                EventLoopGroup bossGroup = new NioEventLoopGroup();
                EventLoopGroup workerGroup = new NioEventLoopGroup();
                ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
                        0,
                        200,
                        60L,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>(2000),
                        new ThreadFactory() {
                            @Override
                            public Thread newThread(Runnable r) {
                                return new Thread(r, "xxl-job, EmbedServer bizThreadPool-" + r.hashCode());
                            }
                        },
                        new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
                            }
                        });
                try {
                    // start server
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    bootstrap.group(bossGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .childHandler(new ChannelInitializer<SocketChannel>() {
                                @Override
                                public void initChannel(SocketChannel channel) throws Exception {
                                    channel.pipeline()
                                            .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                                            .addLast(new HttpServerCodec())
                                            .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                            .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
                                }
                            })
                            .childOption(ChannelOption.SO_KEEPALIVE, true);

                    // bind 异步绑定port上
                    ChannelFuture future = bootstrap.bind(port).sync();

                    logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

                    // start registry  //注册
                    startRegistry(appname, address);

                    // wait util stop
                    future.channel().closeFuture().sync();

                } catch (InterruptedException e) {
                    logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
                } catch (Exception e) {
                    logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
                } finally {
                    // stop
                    try {
                        workerGroup.shutdownGracefully();
                        bossGroup.shutdownGracefully();
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });
        thread.setDaemon(true);    // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
        thread.start();
    }

向服务端注册,默认30秒执行一次

    public void startRegistry(final String appname, final String address) {
        // start registry
        ExecutorRegistryThread.getInstance().start(appname, address);
    }
    public void start(final String appname, final String address){

        // valid appname不允许为null
        if (appname==null || appname.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null.");
            return;
        }//服务端地址不能为null
        if (XxlJobExecutor.getAdminBizList() == null) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null.");
            return;
        }

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

                // registry
                while (!toStop) {
                    try {
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                            try {
                                ReturnT<String> registryResult = adminBiz.registry(registryParam); //向server注册服务(http请求),注册内容appname,当前服务监听地址
                                if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) { //访问成功
                                    registryResult = ReturnT.SUCCESS;
                                    logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                    break;
                                } else {
                                    logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                }
                            } catch (Exception e) {
                                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
                            }

                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
                        if (!toStop) { //心跳时间30秒
                            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                        }
                    } catch (InterruptedException e) {
                        if (!toStop) {
                            logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
                        }
                    }
                }

                // registry remove  删除注册
                try {
                    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                        try {  
                            ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                                registryResult = ReturnT.SUCCESS;
                                logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                break;
                            } else {
                                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                            }
                        } catch (Exception e) {
                            if (!toStop) {
                                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                            }

                        }

                    }
                } catch (Exception e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor registry thread destory.");

            }
        });
        registryThread.setDaemon(true);
        registryThread.setName("xxl-job, executor ExecutorRegistryThread");
        registryThread.start();
    }

2、服务注册

分布式任务调度系列 - XXL-JOB_第22张图片

2.1、任务执行器

com.xxl.job.core.thread.ExecutorRegistryThread#start

    public void start(final String appname, final String address){
    	...
        registryThread = new Thread(new Runnable() {
            @Override
            public void run() {

                // registry
                while (!toStop) {
                    try {
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        // 遍历所有的调度中心
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                            try {
                                ReturnT<String> registryResult = adminBiz.registry(registryParam);
                                if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                                    registryResult = ReturnT.SUCCESS;
                                    logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                    break;
                                } else {
                                    logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                }
                            } catch (Exception e) {
                                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
                            }

                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
                    	// 休眠30s,每30s执行一次
                        if (!toStop) {
                            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                        }
                    } catch (InterruptedException e) {
                        if (!toStop) {
                            logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
                        }
                    }
                }

                // registry remove
                // 线程终止后,主动断开连接
                try {
                    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                        try {
                            ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                                registryResult = ReturnT.SUCCESS;
                                ...
                                break;
                            } else {
                                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                            }
                        } catch (Exception e) {
                            if (!toStop) {
                                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                            }
                        }
                    }
                } catch (Exception e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                ...
            }
        });
        // 设置为守护线程
        registryThread.setDaemon(true);
        registryThread.setName("xxl-job, executor ExecutorRegistryThread");
        registryThread.start();
    }

再来看看其RPC调用,采用的是HTTP传输协议,并采用了JSON作为序列化。

 @Override
 public ReturnT<String> registry(RegistryParam registryParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
 }
 // 可以再细看 com.xxl.job.core.util.XxlJobRemotingUtil,postBody采用就是Http协议,GsonTool将对象转成JSON。

2.2、调度中心

再看看调度中心如何接收任务执行器请求的;
JobApiController就为SpringMVCController,负责接收请求映射

  @RequestMapping("/{uri}")
  @ResponseBody
  @PermissionLimit(limit=false)
  public ReturnT<String> api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) {

      // valid
      if (!"POST".equalsIgnoreCase(request.getMethod())) {
          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 (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null
              && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0
              && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) {
          return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
      }

      // services mapping
      /**
       * 1. 更新调度日志状态;
       * 2. 当执行器执行成功并且存在有子任务时,触发执行子任务
       */
      if ("callback".equals(uri)) {
          List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
          return adminBiz.callback(callbackParamList);
      }
      // 服务注册
      else if ("registry".equals(uri)) {
          RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
          return adminBiz.registry(registryParam);
      }
      // 服务下线
      else if ("registryRemove".equals(uri)) {
          RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
          return adminBiz.registryRemove(registryParam);
      } else {
          return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
      }
  }


	public ReturnT<String> registry(RegistryParam registryParam) {
		// valid  校验
		if (!StringUtils.hasText(registryParam.getRegistryGroup())
				|| !StringUtils.hasText(registryParam.getRegistryKey())
				|| !StringUtils.hasText(registryParam.getRegistryValue())) {
			return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
		}
		// async execute 异步注册
		registryOrRemoveThreadPool.execute(new Runnable() {
			@Override
			public void run() { //更新修改时间
				int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
				if (ret < 1) {//说明暂未数据,才新增			XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
					// fresh  空实现
					freshGroupRegistryInfo(registryParam);
				}
			}
		});
		return ReturnT.SUCCESS;
	}

3、主动触发

分布式任务调度系列 - XXL-JOB_第23张图片

3.1、调度中心

触发地址:com.xxl.job.admin.controller.JobInfoController#triggerJob

	@RequestMapping("/trigger")
	@ResponseBody
	//@PermissionLimit(limit = false)
	public ReturnT<String> triggerJob(int id, String executorParam, String addressList) {
		// force cover job param 设置默认值
		if (executorParam == null) {
			executorParam = "";
		}
		// 触发器类型,手动 ,重试次数,'执行器任务分片参数,格式如 1/2',任务参数,机器地址
		JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList);
		return ReturnT.SUCCESS;
	}



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

        // choose thread pool  获取线程池
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        // 获取超时次数
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        // 一分钟内超时10次,则采用慢触发器执行
        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 {
                    // do trigger // 执行触发器
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
                } 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
                        // 执行时间超过500ms,则记录执行次数
                        AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                        if (timeoutCount != null) {
                            timeoutCount.incrementAndGet();
                        }
                    }

                }

            }
        });
    }

注意当触发器在一分钟内超时10次,则采用慢触发器执行

 private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){

        // param 获取阻塞处理策略
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION);  // block strategy
        // 获取路由策略,默认first
        ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null);    // route strategy
        String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null;

        // 1、save log-id  保存执行日志
        XxlJobLog jobLog = new XxlJobLog();
        jobLog.setJobGroup(jobInfo.getJobGroup());
        jobLog.setJobId(jobInfo.getId());
        jobLog.setTriggerTime(new Date());
        XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog);
        logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());

        // 2、init trigger-param
        TriggerParam triggerParam = new TriggerParam();
        triggerParam.setJobId(jobInfo.getId());
        triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
        triggerParam.setExecutorParams(jobInfo.getExecutorParam());
        triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
        triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
        triggerParam.setLogId(jobLog.getId());
        triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime());
        triggerParam.setGlueType(jobInfo.getGlueType());
        triggerParam.setGlueSource(jobInfo.getGlueSource());
        triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
        triggerParam.setBroadcastIndex(index);
        triggerParam.setBroadcastTotal(total);

        // 3、init address 获取触发器执行地址
        String address = null;
        ReturnT<String> routeAddressResult = null;
        if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
            // 路由策略为分配广播
            if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
                if (index < group.getRegistryList().size()) {
                    address = group.getRegistryList().get(index);
                } else {
                    address = group.getRegistryList().get(0);
                }
            } else {
                // 根据设置的路由策略,执行路由器,获取返回结果,这里用到了策略模式
                /**
                 *  1. ExecutorRouteFirst (第一个)固定选择第一个机器
                 *  2. ExecutorRouteLast (最后一个)
                 *  3. ExecutorRouteRound (轮询), 通过Map记录任务的执行次数进行取模
                 *  4. ExecutorRouteRandom (随机)
                 *  5. ExecutorRouteConsistentHash (一致性hash),每个jobId都会hash到指定的机器上,每次都会构建虚拟节点
                 *  6. ExecutorRouteLFU (最不频繁使用,1天的使用频繁), 通过Map存储每个jobId在每个地址的使用次数,拿到最少使用地址;
                 *  7. ExecutorRouteLRU (最近最久未使用), 通过LinkedHashMap accessOrder进行实现,其内部通过双向链表实现
                 *  8. ExecutorRouteFailover(故障转移) 通过顺序遍历执行器地址,进行心跳检查
                 *  9. ExecutorRouteBusyover(忙碌转移) 照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
                 */
                routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
                if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
                    address = routeAddressResult.getContent();
                }
            }
        } else {
            routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
        }

        // 4、trigger remote executor
        ReturnT<String> triggerResult = null;
        if (address != null) {
        	// 已经获取到任务执行器地址,通过HTTP进行调度
            triggerResult = runExecutor(triggerParam, address);
        } else {
            triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null);
        }

        // 5、collection trigger info
        StringBuffer triggerMsgSb = new StringBuffer();
        triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle());
        triggerMsgSb.append("
"
).append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp()); triggerMsgSb.append("
"
).append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":") .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") ); triggerMsgSb.append("
"
).append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList()); triggerMsgSb.append("
"
).append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()); if (shardingParam != null) { triggerMsgSb.append("("+shardingParam+")"); } triggerMsgSb.append("
"
).append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle()); triggerMsgSb.append("
"
).append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout()); triggerMsgSb.append("
"
).append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount); triggerMsgSb.append("

:#00c0ef;" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<<
"
) .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"

"
:"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():""); // 6、save log trigger-info jobLog.setExecutorAddress(address); jobLog.setExecutorHandler(jobInfo.getExecutorHandler()); jobLog.setExecutorParam(jobInfo.getExecutorParam()); jobLog.setExecutorShardingParam(shardingParam); jobLog.setExecutorFailRetryCount(finalFailRetryCount); //jobLog.setTriggerTime(); jobLog.setTriggerCode(triggerResult.getCode()); jobLog.setTriggerMsg(triggerMsgSb.toString()); XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog); logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); }

JobThread执行调度逻辑
com.xxl.job.core.thread.JobThread#run()

 @Override
	public void run() {

    	// init
    	try {
    		// 执行初始化方法(初始化连接池等信息,一个job只能执行一次)
			handler.init();
		} catch (Throwable e) {
    		logger.error(e.getMessage(), e);
		}

		// execute
		while(!toStop){
			running = false;
			idleTimes++;

            TriggerParam triggerParam = null;
            try {
				// to check toStop signal, we need cycle, so wo cannot use queue.take(), instand of poll(timeout)
				// 从队列中获取调度日志
				triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
				if (triggerParam!=null) {
					running = true;
					idleTimes = 0;
					triggerLogIdSet.remove(triggerParam.getLogId());

					// log filename, like "logPath/yyyy-MM-dd/9999.log" 写入log文件
					String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
					XxlJobContext xxlJobContext = new XxlJobContext(
							triggerParam.getJobId(),
							triggerParam.getExecutorParams(),
							logFileName,
							triggerParam.getBroadcastIndex(),
							triggerParam.getBroadcastTotal());

					// init job context
					XxlJobContext.setXxlJobContext(xxlJobContext);

					// execute
					XxlJobHelper.log("
----------- xxl-job job execute start -----------
----------- Param:"
+ xxlJobContext.getJobParam()); // 设置了超时就异步线程处理(FutureTask设置超时时间) if (triggerParam.getExecutorTimeout() > 0) { // limit timeout Thread futureThread = null; try { FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() { @Override public Boolean call() throws Exception { // init job context XxlJobContext.setXxlJobContext(xxlJobContext); handler.execute(); return true; } }); futureThread = new Thread(futureTask); futureThread.start(); Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS); } catch (TimeoutException e) { XxlJobHelper.log("
----------- xxl-job job execute timeout"
); XxlJobHelper.log(e); // handle result XxlJobHelper.handleTimeout("job execute timeout "); } finally { futureThread.interrupt(); } } else { // just execute // 反射,invoke handler; 没设置超时时间,则立刻执行触发器 handler.execute(); } // 记录执行日志 // valid execute handle data if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) { XxlJobHelper.handleFail("job handle result lost."); } else { String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg(); tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000) ?tempHandleMsg.substring(0, 50000).concat("...") :tempHandleMsg; XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg); } XxlJobHelper.log("
----------- xxl-job job execute end(finish) -----------
----------- Result: handleCode="
+ XxlJobContext.getXxlJobContext().getHandleCode() + ", handleMsg = " + XxlJobContext.getXxlJobContext().getHandleMsg() ); } else { // 空闲执行次数超过30次,且队列没任务,则删除并终止线程 if (idleTimes > 30) { if(triggerQueue.size() == 0) { // avoid concurrent trigger causes jobId-lost XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit."); } } } } /** * 当任务调度有异常时,捕捉异常,通过XxlJobHelper.handleFail(errorMsg)设置失败; * 所以当JobHandler处理业务逻辑时,记得抛出异常 */ catch (Throwable e) { if (toStop) { XxlJobHelper.log("
----------- JobThread toStop, stopReason:"
+ stopReason); } // handle result StringWriter stringWriter = new StringWriter(); e.printStackTrace(new PrintWriter(stringWriter)); String errorMsg = stringWriter.toString(); XxlJobHelper.handleFail(errorMsg); XxlJobHelper.log("
----------- JobThread Exception:"
+ errorMsg + "
----------- xxl-job job execute end(error) -----------"
); } finally { if(triggerParam != null) { // callback handler info // 添加回调队列 if (!toStop) { // commonm TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.getXxlJobContext().getHandleCode(), XxlJobContext.getXxlJobContext().getHandleMsg() ) ); } else { // is killed TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.HANDLE_CODE_FAIL, stopReason + " [job running, killed]" ) ); } } } }

4. 自动触发

分布式任务调度系列 - XXL-JOB_第24张图片
分布式任务调度系列 - XXL-JOB_第25张图片

4.1、自动触发逻辑

com.xxl.job.admin.core.thread.JobScheduleHelper#start()
scheduleThread定时线程

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

               try {
                   // 保证5秒执行一次
                   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)
               // 每秒处理20个任务,200个线程
               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();
                       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();
                       //获取当前时间后5秒,同时最多负载的分页数
                       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(时间轮)
                               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 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 下次触发时间在当前时间往后5秒范围内
                                   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

                                   // 未来五秒以内执行的所有任务添加到ringData
                                   // 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;
                           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();

4.2、时间轮线程

com.xxl.job.admin.core.thread.JobScheduleHelper#start()
ringThread时间轮线程

// 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);
                       }
                   }
               }
               logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
           }
       });
       ringThread.setDaemon(true);
       ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
       ringThread.start();

5、设计亮点

  1. 路由策略
    1. 路由策略使用了 策略设计模式,根据选择的策略去获取对应的调度中心地址;
    2. 支持了首个、最后、随机、轮询、一致性hash、LRU、LFU、故障转移、忙碌转移、分配广播;
  2. 注册中心
    1. 续期线程每30秒对任务执行器进行续期
    2. 过期线程每30秒把90未续期的任务执行器移除;
  3. 全异步化 & 轻量级
    1. 调度中心
      1. 调度任务:线程定时获取要执行的任务,并交给调度线程池异步调用;
      2. 心跳: 新开线程清理过期的任务执行器;
      3. 失败任务:线程重试并告警;
    2. 任务执行器
      1. 执行任务: 每个job任务都有各自jobThread从队列中获取;
      2. 回调: 有两个线程 回调和重试线程,负责向xxlAdmin回调任务执行状态;
      3. 心跳: 新开线程每隔30s进行续期
    3. 异步化
      1. 异步调用:交给线程池进行异步调用任务给任务执行器
      2. 异步执行:任务执行器每个job都有各自的线程,并异步回调给xxlAdmin;
    4. 轻量级
      1. 架构上非常的轻,基本通过Mysql实现了分布式锁、注册中心、任务调度等功能,只需依赖Mysql + Java;
      2. 在全异步化的基础上,单个JOB一次运行平均耗时基本在 “10ms” 之内(基本为一次请求的网络开销),可以保证使用有限的线程支撑大量的JOB并发运行;
      3. 官方文档表示,在理论的调度中心下,单机能支撑5000任务并发;
      4. 如何提高性能:1. 机器上;2. 不同业务进行区分; 3. 修改源码(不同的xxl-job集群处理不同的job)
  4. 时间轮算法
    1. 是什么
      1. 时间轮方案将现实生活中的时钟概念引入到软件设计中,主要思路是定义一个时钟周期(比如时钟的12小时)和步长(比如时钟的一秒走一次),当指针每走一步的时候,会获取当前时钟刻度上挂载的任务并执行。
      2. 一个环形数组存储时间轮的所有槽(看你的手表),每个槽对应当前时间轮的最小精度
        超过当前时间轮最大表示范围的会被丢到上层时间轮,上层时间轮的最小精度即为下层时间轮能表达的最大时间(时分秒概念)
      3. 每个槽对应一个环形链表存储该时间应该被执行的任务
      4. 需要一个线程去驱动指针运转,获取到期任务
    2. xxl-job实现
      1. xxl-job的时间环只会存储之后5s内执行的任务,使用一个Map进行存储;
      2. Map的key为执行时间的秒数%60,value为这个秒执行的jobIdList;
      3. 时间轮线程每1秒执行一次,从时间轮从获取到jobIdList,最后进行调度任务;

你可能感兴趣的:(中间件,分布式,java,数据库)