开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情
前言
业务场景
业务数据同步(线上数据同步至线下,新老平台的数据同步),消息同步(花呗账单,信用卡账单),业务数据补偿等等。
什么是定时任务
定时任务是基于给定的时间点、给定的时间间隔或者执行次数自动执行的程序。任务调度系统是系统的重要组成部分,对于实时的系统,任务调度直接影响着系统的实时性。任务调度设计到多线程并发,运行时间规则定制以及解析、线程池维护等诸多方面的工作。
一般定时任务的不足
- 不支持集群
- 不支持任务重试
- 不支持动态调用规则
- 无报警机制
- 不支持生命周期的统一管理
- 任务数据难以统计
xxl-job定时任务
首先将程序分为调度器与执行器两个模块。执行器指的就是当前应用程序,应用程序中包含了所需的任务模块。执行器中包含了注册线程,调度器会记录ip到数据中心,执行器管理会记录。执行器中的任务会被调度中心的任务管理模块所管理。调度中心的调度器调用任务执行。
详细可以看官方文档了解分布式任务调度平台XXL-JOB (xuxueli.com)
启动xxl-job-admin
1、在Gitee-xxl-job 或者 Github-xxl-job 拉取xxl-job的项目源码
2、在本地MySQL中执行**/xxl-job/doc/db/tables_xxl_job.sql**的SQL脚本
3、修改xxl-job-admin调度中心的数据库配置(主要修改数据库账号密码配置)
笔者使用的是mac,在启动时报错,提示无法创建日志文件,需在logback.xml中做修改
在/data/applogs/xxl-job/xxl-job-admin.log前添加., 重启就不会报错了!
调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
1、将调度行为抽象形成调度中心公共平台,平台本身不承担业务逻辑,只是负责发起请求。
2、将任务抽象成分散的JobHandler,交由执行器统一管理,执行器负责接收调度请求并执行对应JabHandler中的业务逻辑
因此调度与任务互相解耦,提高系统整体的稳定性和扩展性。
springboot整合
引入依赖
<!--xxl-job依赖-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件
# web port
server.port=8081
# 日志文件配置
logging.config=classpath:logback.xml
### xxl-job-admin地址
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job, access token,可以修改,但是务必要和xxl-job-admin中的配置一致
xxl.job.accessToken=default_token
### xxl-job executor appname
xxl.job.executor.appname=xxl-job-Odin
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
### 空着不填,在xxl-job-admin的管理页面中添加执行器时会自动扫描注入
xxl.job.executor.address=
### xxl-job executor server-info
### 空着不填,在xxl-job-admin的管理页面中添加执行器时会自动扫描注入
xxl.job.executor.ip=
# 执行器内部端口rpc
xxl.job.executor.port=8181
### xxl-job executor log-path
xxl.job.executor.logpath=./data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="./data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
踩坑:由于使用的是mac,所以需要在日志目录前添加.,否则会报异常
注:此配置针对的是
2.3.0版本以上的配置文件
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@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.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
定时任务demo
@Component
public class DemoJob {
// 定时任务名称,必填
@XxlJob(value = "getTime")
public ReturnT demoTask() {
System.out.println(System.currentTimeMillis());
// 调度结果(默认返回success)
return ReturnT.SUCCESS;
}
}
xxl-job-admin Web端配置
- 执行器管理
AppName需与配置文件中保持一致,注册方式选择自动注入,机器地址即可不用填写。
保存后等待一段时间,出现查看即注册成功。
- 任务管理
任务描述需与程序应用中,注解填写的value保持一致
点击保存即可
点击操作即可执行定时任务
可以在调度日志中查看执行状态,也可到程序应用中查看日志。
至此完成一个简单的定时任务应用。
集群调度策略
路由策略种类:
- 第⼀个:当选择该策略时,会选择执⾏器注册地址的第⼀台机器执⾏,如果第⼀台机器出现故障,则调度任务失败。
- 最后⼀个:当选择该策略时,会选择执⾏器注册地址的最后⼀台机器执⾏,如果最后⼀台机器出现故障,则调度任务失败。
- 轮询:当选择该策略时,会按照执⾏器注册地址轮询分配任务,如果其中⼀台机器出现故障,调度任务失败,任务不会转移。
- 随机:当选择该策略时,会按照执⾏器注册地址随机分配任务,如果其中⼀台机器出现故障,调度任务失败,任务不会转移。
- ⼀致性HASH:当选择该策略时,每个任务按照Hash算法固定选择某⼀台机器。如果那台机器出现故障,调度任务失败,任务不会转移。
- 最不经常使⽤:当选择该策略时,会优先选择使⽤频率最低的那台机器,如果其中⼀台机器出现故障,调度任务失败,任务不会转移。
- 最近最久未使⽤:当选择该策略时,会优先选择最久未使⽤的机器,如果其中⼀台机器出现故障,调度任务失败,任务不会转移。
- 故障转移:当选择该策略时,按照顺序依次进⾏⼼跳检测,如果其中⼀台机器出现故障,则会转移到下⼀个执⾏器,若⼼跳检测成功,会选定为⽬标执⾏器并发起调度。
- 忙碌转移:当选择该策略时,按照顺序依次进⾏空闲检测,如果其中⼀台机器出现故障,则会转移到下⼀个执⾏器,若空闲检测成功,会选定为⽬标执⾏器并发起调度。
- 分⽚⼴播:当选择该策略时,⼴播触发对应集群中所有机器执⾏⼀次任务,同时系统⾃动传递分⽚参数;可根据分⽚参数开发分⽚任务。如果其中⼀台机器出现故障,则该执⾏器执⾏失败,不会影响其他执⾏器。
父子任务
xxl-job支持父子任务,即先执行父任务,然后立即执行子任务
在注册完成父子任务后,取得子任务的任务id,填入父任务即可。
动态参数任务
业务代码
@XxlJob(value = "paramTask")
public ReturnT paramTask() {
String data = DateUtil.today();
// 通过 XxlJobHelper.getJobParam() 获取调度中心传入的参数
String param = XxlJobHelper.getJobParam();
if (StrUtil.isNotBlank(param)) {
data = param;
}
System.out.println("业务处理时间为"+data);
return ReturnT.SUCCESS;
}
可在此处填写参数,下图是填写与不填写任务参数的对比。
分片任务
分片任务是指对所有执行器广播这个任务,所有执行器都会收到调用请求,每个执行器可以根据总分片数以及当前执行器数的索引进行相关业务的处理。
/**
* 分片任务:10万数据
* 总分片数
* 当前分片索引
* 每个分片平均处理数量:10万数据/总分片数
* 每个分片处理的范围:
* 开始索引:当前分片索引 * 平均处理数据量 + 1
* 结束索引:(当前的分片索引 + 1) * 均处理的数据量
*/
@XxlJob(value = "shardTask")
public ReturnT shardTask() {
System.out.println("===============演示分片任务=================");
// 获取分片信息
// 总分片数量
int shardTotal = XxlJobHelper.getShardTotal();
// 当前分片的索引
int shardIndex = XxlJobHelper.getShardIndex();
// 总数据量
int total = 10 * 10000;
// 分片平均数据量
int size = total / shardTotal;
// 每个分片起始值
int startIndex = shardIndex * size + 1;
int endIndex = (startIndex + 1) * size;
// 处理最后一个分片,最后一个分片要处理到最后一条数据
if (shardIndex == (shardTotal - 1)) {
endIndex = total;
}
log.info("总分片数为:{},当前分片索引为:{}处理数据的范围:{}~{}", shardTotal, shardIndex, startIndex, endIndex);
return ReturnT.SUCCESS;
}
任务生命周期
@XxlJob(value = "lifeCycle",init = "init",destroy = "destroy")
public ReturnT lifeCycle() {
System.out.println("任务执行");
}
public void init() {
System.out.println("init.....");
}
public void destroy() {
System.out.println("destroy.....");
}
init是当前任务开始第一次执行时会触发,destroy是当执行器也就是程序应用停止时会执行。