1、什么是XXL_JOB
---------XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。开箱即用。
概念:
xxl-job是一个轻量级分布式任务调度平台,xxl-job其实也是在quartz的基础上实现的,但是修改了任务调度的模式,并且任务调度采用注册和RPC调用方式来实现。
XXL_JOB优势:
1. xxl_job支持任务集群,通过Job 负载均衡轮询机制保证幂等性问题
2. job在最新版本中支持Job补偿,如果Job执行失败的话,可自动重试、重试数次失败后可发送预警邮件
3. 支持Job日志记录
4. 动态配置定时规则,传统定时Job触发规则都是写死在代码中
5. “调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
6. 为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯
2、XXL_JOB的作用
解决需要定时或者间隔一定时间就需要执行的业务,类似于定时清除日志、定期发送报表数据、活动推送等
3、xxl_Job的怎么用
XXL_JOB有两大主要概念;
调度器:统一管理任务调度的平台,负责转发任务到对应的执行服务器;
执行器:定时Job实际执行定时任务的服务器地址;
配置调度器:
因为任务调度的内容和日志需要持久化,所以需要配置数据库,集成预警后需要配置预警方式和国际化配置以及调度线程池最大线程配置
### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
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_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
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
配置执行器:
需要把执行器注册到调度中心,要配置调度中心地址:
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”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
实现流程:
1、首先将执行器注册在调度中心后,在调度中心的Web管理平台上新建调度任务,可选择相应的执行器、路由策略、运行模式和cron表达式
2、启动/停止任务
3、查看调度日志
4、查看调度日志
5、当返回值符合 “ReturnT.code == ReturnT.SUCCESS_CODE” 时表示任务执行成功,否则表示任务执行失败,而且可以通过 “ReturnT.msg” 回调错误信息给调度中心;
五种部署执行器执行内容的方式:
1、BEAN模式(一般均采用这种):
1.1类形式
Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。
1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
2、手动通过如下方式注入到执行器容器。
```
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
```
1.2方法形式
Bean模式任务,支持基于方法的开发方式,每个任务对应一个方法。
1、任务开发:在Spring Bean实例中,开发Job方法;
2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
@XxlJob("demoJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
return ReturnT.SUCCESS;
}
2、GLUE模式(Java)
任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。
3、GLUE模式(Shell)
4、GLUE模式(Python)
5、GLUE模式(NodeJS)
6、GLUE模式(PHP)
7、GLUE模式(PowerShell)
4、XXL_JOB的应用原理
Bean模式任务,支持基于方法的开发方式:
#基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。
@XxlJob
public ReturnT<String> Test1(String param) {
XxlJobLogger.log("hello world");
System.out.println("我是XXL_job次执行");
return ReturnT.SUCCESS;
}
1、执行器启动与注册
注册器的注册与发现有两种方式:
- 一种是执行器启动时,主动到注册中心注册,并定时发送心跳,保持续约。执行器正常关闭时,也主动告知调度中心注销。这种方式叫做
主动注册
。
- 如果执行器宕机或者网络出现问题,调度中心本身也需要不断的对执行器进行
探活
(类似于RocketMQ中的NameServer和Broker)。调度中心会启动一个专门探活的后台线程,定时调用执行器接口,如果发现异常就将执行器下线,避免路由到一个不可用的执行器导致任务失败。
1.1XxlJobSpringExecutor
在执行器端我们从XXL-JOB的配置类XxlJobConfig触发,这里用到了我们配置在application.properties的参数。
配置类中定义了一个XxlJobSpringExecutor,会在启动扫描配置类的时候创建Bean。
点进此类发现集成了XxlJobExecutor,又实现了Spring中的SmartInitializingSingleton
接口,此接口在对象初始化的时候会调用afterSingletonInstantiated()方法对执行器进行初始化。
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, InitializingBean, DisposableBean {
// start
@Override
public void afterPropertiesSet() throws Exception {
// init JobHandler Repository
//扫描注解,注册到任务库中
initJobHandlerRepository(applicationContext);
// init JobHandler Repository (for method)
//扫描方法,注册到任务库
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory
GlueFactory.refreshInstance(1);
// super start
super.start();
}
// destroy
@Override
public void destroy() {
super.destroy();
}
/**
省略
private void initJobHandlerRepository(ApplicationContext applicationContext)
private void initJobHandlerMethodRepository(ApplicationContext applicationContext)
*/
// ---------------------- applicationContext ----------------------
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
1.1.1 initJobHandlerMethodRepository(applicationContext);
此方法中拿到所有IOC容器中的bean,并扫描看其中的方法是否有带@XxlJob注解的得到一个Map<Method,XxlJob> annotatedMethods ;遍历所有的方法进行一些校验(JobHandler不能重复,不能为空,方法格式是否正确,拿到handler的前置方法和后置方法),最后进行注册。
private void initJobHandlerRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
// init job handler action
Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(JobHandler.class);
if (serviceBeanMap != null && serviceBeanMap.size() > 0) {
for (Object serviceBean : serviceBeanMap.values()) {
if (serviceBean instanceof IJobHandler) {
String name = serviceBean.getClass().getAnnotation(JobHandler.class).value();
IJobHandler handler = (IJobHandler) serviceBean;
if (loadJobHandler(name) != null) {
throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
}
//注册到jobHandlerRepository中
registJobHandler(name, handler);
}
}
}
}
// ---------------------- job handler repository ----------------------
//注册比较简单,其实就是将JobHandler放到一个map中,map的key就是jobHandler的名字,value就是jobhandler。
private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler);
return jobHandlerRepository.put(name, jobHandler);
}
1.1.2 initJobHandlerMethodRepository(applicationContext);
//扫描方法,注册到任务库
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
// init job handler from method
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
Method[] methods = bean.getClass().getDeclaredMethods();
for (Method method: methods) {
XxlJob xxlJob = AnnotationUtils.findAnnotation(method, XxlJob.class);
if (xxlJob != null) {
// name
String name = xxlJob.value();
if (name.trim().length() == 0) {
throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + bean.getClass() + "#"+ method.getName() +"] .");
}
if (loadJobHandler(name) != null) {
throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
}
// execute method
if (!(method.getParameterTypes()!=null && 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<String> 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<String> execute(String param) \" .");
}
method.setAccessible(true);
// init and destory
Method initMethod = null;
Method destroyMethod = null;
if(xxlJob.init().trim().length() > 0) {
try {
initMethod = bean.getClass().getDeclaredMethod(xxlJob.init());
initMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + bean.getClass() + "#"+ method.getName() +"] .");
}
}
if(xxlJob.destroy().trim().length() > 0) {
try {
destroyMethod = bean.getClass().getDeclaredMethod(xxlJob.destroy());
destroyMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + bean.getClass() + "#"+ method.getName() +"] .");
}
}
// registry jobhandler
registJobHandler(name, new MethodJobHandler(bean, method, initMethod, destroyMethod));
}
}
}
}
1.1.3 GlueFactory.refreshInstance(1);
private static GlueFactory glueFactory = new GlueFactory();
public static GlueFactory getInstance(){
return glueFactory;
}
public static void refreshInstance(int type){
if (type == 0) {
glueFactory = new GlueFactory();
} else if (type == 1) {
glueFactory = new SpringGlueFactory();
}
}
1.1.4 super.start();
//注册扫描完所有的jobhandler之后,就运行到XxlJobSpringExecutor的父类XxlJobExecutor的start()方法中。
// ---------------------- start + stop ----------------------
public void start() throws Exception {
// init logpath
init logpath 初始化日志路径
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client
// init invoker, admin-client 创建调度器客户端
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
// init JobLogFileCleanThread 初始化日志清理线程
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
// init TriggerCallbackThread 初始化回调线程
TriggerCallbackThread.getInstance().start();
// init executor-server
// init executor-server 初始化执行器服务
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
initRpcProvider(ip, port, appName, accessToken);
}
1.1.4.1 initAdminBizList
- initAdminBizList创建调度器客户端,是执行器用来连接调度器的,可以获得所有的调度器地址并封装到一个list中。
// ---------------------- admin-client (rpc invoker) ---------------------- private static List<AdminBiz> adminBizList; private static Serializer serializer = new HessianSerializer(); private void initAdminBizList(String adminAddresses, String accessToken) throws Exception { if (adminAddresses!=null && adminAddresses.trim().length()>0) { for (String address: adminAddresses.trim().split(",")) { if (address!=null && address.trim().length()>0) { AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken); if (adminBizList == null) { adminBizList = new ArrayList<AdminBiz>(); } adminBizList.add(adminBiz); } } } }