项目场景中,往往许多场景中需要分布式任务调度,而目前基于springcloud的项目中,因业务需求逐步的完善的,许多模块中也添加了自己的定时任务,除了代码维护性低,也存在以下问题:
(1)同一服务多个实例的任务存在互斥时,需要统一协调
(2)定时任务的执行需要支持高可用、监控运维、故障告警
(3)需要统一管理和追踪各个服务节点定时任务的运行情况,以及任务属性信息,例如任务所属服务、所属责任人
调研了目前比较流行的几个任务调度框架,具体比较如下(图摘自网络):
xxl-job
1、源码下载地址
①、GitHub:https://github.com/xuxueli/xxl-job
②、码云:https://gitee.com/xuxueli0323/xxl-job
2、文档地址
①、中文文档:http://www.xuxueli.com/xxl-job/#/
②、英文文档:http://www.xuxueli.com/xxl-job/en/#/
从比较中可以看出,xxl-job开发迅速、学习简单、轻量级、易扩展、开箱即用,具有以下特性:
简单灵活 提供Web页面对任务进行管理,管理系统支持用户管理、权限控制; 支持容器部署; 支持通过通用HTTP提供跨平台任务调度;
丰富的任务管理功能 支持页面对任务CRUD操作; 支持在页面编写脚本任务、命令行任务、Java代码任务并执行; 支持任务级联编排,父任务执行结束后触发子任务执行; 支持设置任务优先级; 支持设置指定任务执行节点路由策略,包括轮询、随机、广播、故障转移、忙碌转移等; 支持Cron方式、任务依赖、调度中心API接口方式触发任务执行
高性能 调度中心基于线程池多线程触发调度任务,快任务、慢任务基于线程池隔离调度,提供系统性能和稳定性; 任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰;
高可用 任务调度中心、任务执行节点均 集群部署,支持动态扩展、故障转移 支持任务配置路由故障转移策略,执行器节点不可用是自动转移到其他节点执行 支持任务超时控制、失败重试配置 支持任务处理阻塞策略:调度当任务执行节点忙碌时来不及执行任务的处理策略,包括:串行、抛弃、覆盖策略
易于监控运维 支持设置任务失败邮件告警,预留接口支持短信、钉钉告警; 支持实时查看任务执行运行数据统计图表、任务进度监控数据、任务完整执行日志;
调度模块(调度中心): 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover
执行模块(执行器): 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收“调度中心”的执行请求、终止请求和日志请求等
任务执行器根据配置的调度中心的地址,自动注册到调度中心
达到任务触发条件,调度中心下发任务
执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中
执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心
当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情
1.首先下载源代码,导入项目模块中
2.配置文件
因为需要将这份代码,转变成springcloud中独立服务使用,所以需要根据项目的习惯做一样的修改,以下是针对xxl-job-admin模块的说明:
数据库初始化脚本执行后,bootstrap.yml配置必须的配置,如数据库,消息中间件等,application.yml如下
server:
port: 9997
#actuator
servlet:
context-path: /xxl-job-admin
management:
server:
servlet:
context-path: /actuator
health:
mail:
enabled: false
### mybatis
mybatis:
mapper-locations: /mybatis-mapper/*Mapper.xml
spring:
mvc:
static-path-pattern: /static/**
servlet:
load-on-startup: 0
resources:
static-locations: classpath:/static/
### freemarker
freemarker:
templateLoaderPath: classpath:/templates/
suffix: .ftl
charset: UTF-8
request-context-attribute: request
settings.number_format: 0.##########
### xxl-job, datasource
datasource:
url: jdbc:mysql://${db_ip_port}/xxx?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: ${db_username}
password: ${db_password}
driver-class-name: com.mysql.cj.jdbc.Driver
#这个段配置是源码的,改成了项目使用的配置中心druid
### datasource-pool
# type: com.zaxxer.hikari.HikariDataSource
# hikari:
# minimum-idle: 10
# maximum-pool-size: 30
# auto-commit: true
# idle-timeout: 30000
# pool-name: HikariCP
# max-lifetime: 900000
# connection-timeout: 10000
# connection-test-query: SELECT 1
### xxl-job, email
mail:
host: smtp.qq.com
port: 25
username: [email protected]
password: xxx
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
socketFactory:
class: javax.net.ssl.SSLSocketFactory
### xxl-job, access token
xxl:
job:
accessToken:
### xxl-job, i18n (default empty as chinese, "en" as english)
i18n:
## xxl-job, triggerpool max size
triggerpool:
fast:
max: 200
slow:
max: 100
### xxl-job, log retention days
logretentiondays: 30
3.启动项目
访问地址:http://localhost:9997/xxl-job-admin/toLogin
默认账号密码admin 123456
4.客户端对接
<1>maven依赖
com.xuxueli
xxl-job-core
${project.parent.version}
<2>增加配置信息
其中appname和界面创建的执行器名称要一致,这里取的是服务名称
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl:
job:
admin:
addresses: http://localhost:9997/xxl-job-admin
executor:
appname: ${spring.application.name}
ip:
port: 9999
logpath: /usr/local/logs/${spring.application.name}/scheduling
logretentiondays: 30
accessToken:
<3>增加spring扫描路径
源码中需要在对接的客户端增加初始化配置类,由于每个客户端需要依赖xxl-job-core,所以将配置类放入该依赖模块中,客户端启动类配置扫描路径即可@SpringBootApplication(scanBasePackages = {“xx.xxx.xxxxx.configuration”})
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
logger.info(">>>>>>>>>>> 定时任务配置初始化");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
*
* org.springframework.cloud
* spring-cloud-commons
* ${version}
*
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
<4>编写需要定时执行的业务代码
(1)类的形式
开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
@JobHandler(value = "cs1DbJobHandler")
@Component
public class Cs1DbJobHandler extends IJobHandler {
@Autowired
ICs1DbService cs1DbService;
@Override
public ReturnT execute(String s){
XxlJobLogger.log("hello world.");
return ReturnT.SUCCESS;
}
}
(2)方法的形式
1、在Spring Bean实例中,开发Job方法,方式格式要求为 “public ReturnT execute(String param)”
2、为Job方法添加注解 “@XxlJob(value=“自定义jobhandler名称”, init = “JobHandler初始化方法”, destroy = “JobHandler销毁方法”)”,注解value值对应的是调度中心新建任务的JobHandler属性的值。
3、执行日志:需要通过 “XxlJobLogger.log” 打印执行日志;
@Component
public class TestSchedualing {
@XxlJob("myFirstDemo")
public ReturnT execute(String param) {
XxlJobLogger.log("hello world.");
return ReturnT.SUCCESS;
}
}
<5>登录平台创建执行器
<6>创建任务
任务中JobHandler就是我们代码中@XxlJob注解的值,这样平台才能找到对应的代码执行,
常用路由策略:
第一个:总在第一个注册的服务上执行
轮询(推荐):每个服务依次执行(每次只有一个)
随机:随机一个节点执行
对cron表达式的补充说明:
cron表达式右边按钮可以自动生成表达式,假如需要生成每小时的定时任务,
1.第一步需要选择小时中每小时执行一次。
2.除了指定每小时的轮询外,需要指定具体的时候进行执行,所以需要在分和秒中选择具体的时间,若不选择,将会每秒执行。当然,也可以选择除了整点以外的时间。注意:如果需要不同的时间点执行,则需要创建多个定时任务,例如每天早上8.30和9.45执行,这样需要分别创建各自的任务。
若有动态创建定时任务的需求,这时候就需要使用接口的形式进行创建,源码也是提供了任务的增删改查的接口,源码路径为
com.xxl.job.admin.controller.JobInfoController
com.xxl.job.admin.controller.JobGroupController
大家可以按需增加接口,例如批量的接口。