1. 定时任务框架xxl-job
1.1. 需求说明
当用户预约之后,假如没有在预约的时间来访,那这次预约就会失效。那我们如何设置失效的状态呢?
肯定不能人工操作,更加简便的方式就是自动完成。那如何自动完成,就是我们要学习的任务调度
1.2. 定时任务概述
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。
常用业务场景案例:
- 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
- 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
- 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
- 12306会根据车次的不同,设置某几个时间点进行分批放票。
如何实现任务调度?
- 多线程方式,结合sleep
- JDK提供的API,例如:Timer、ScheduledExecutor
- 框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
- spring task
- 分布式任务调度框架(例如:xxl-job)推荐使用
1.3. xxl-job入门
1.3.1. xxl-job概述
在我们的项目中,使用的就是xxl-job调度框架来完成任务的自动执行。它是一个分布式的任务调度框架,我们后期要学习到的微服务课程、项目等也都会采用这个调度框架。
那我们为什么现在就会使用分布式的调度框架呢?
大家来看下面这个图:
上面是一个发送优惠券的定时任务
- 如果只是单体项目的话,定时任务执行是不会有任何问题的
- 如果后期业务量较大,单体项目做了集群部署,那集群中每一台服务的代码都是一样的,都会按照规定的时间来执行任务,这样就会造成优惠券重复发放。
所以,由此以上分析,我们要解决就是,即使是单体项目,如果做集群,同样要考虑任务重复执行的问题,那xxl-job就可以解决这些问题,当然不仅仅如此
官网地址:分布式任务调度平台XXL-JOB
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
xxl-job架构图(官图):
1.3.2. 环境搭建
在提供的虚拟机中已经通过docker部署了xxl-job服务,我们之间访问即可,访问地址:http://192.168.200.146:8888/xxl-job-admin/
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.200.146:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 \
--spring.datasource.username=root \
--spring.datasource.password=heima123" \
-p 8888:8080 \
-v /tmp:/data/applogs \
--name xxl-job-admin \
--restart=always -d \
xuxueli/xxl-job-admin:2.3.0
#说明
# --spring.datasource.url 指定了xxl-job链接的数据库信息
# -p 指定了端口映射
# -v 指定了目录挂载
# --name 指定了容器的名称
# --restart=always 指定了容器自启动
#xuxueli/xxl-job-admin:2.3.0 指定了镜像版本信息
xxl-job共用到8张表,数据库脚本:doc/db/tables_xxl_job.sql · 许雪里/xxl-job - Gitee.com
- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;
1.3.3. 入门案例编写
1.3.3.1. 代码环境
从虚拟机中的git服务拉取xxl-job示例的基础代码,地址:http://git.zzyl.com/zzyl/zzyl-xxl-job.git
项目配置,其中配置了项目名称、web服务端口、xxl-job相关的配置:
application:
version: v1.0
spring:
application:
name: zzyl-xxl-job
server:
port: 9901
xxl:
job:
admin:
addresses: http://192.168.200.146:8888/xxl-job-admin #xxl-job调度中心地址
executor:
ip: 192.168.200.1 #注册到调度中心的执行器ip地址
appname: ${spring.application.name} #执行器的名称
#执行器运行日志文件存储磁盘路径
logpath: /data/applogs/xxl-job/jobhandler
#执行器日志文件保存天数
logretentiondays: 30
xxl-job的配置类,主要目的是完成XxlJobSpringExecutor
对象的初始化,并且将其交由Spring
管理。
package com.zzyl.xxljob.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* xxl-job config
*/
@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:0}")
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;
}
}
配置说明:
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
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
在xxl-job的调度中心中创建执行器:
将zzyl-xxl-job工程启动后,会发现已经有服务节点注册到了注册中心:
1.3.3.2. 简单任务
下面我们将编写第一个xxl-job的任务,需求:执行一组任务,执行的简单为每5秒钟执行一次。
package com.zzyl.xxljob.job;
import cn.hutool.core.util.RandomUtil;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* 任务处理器
*/
@Component
public class JobHandler {
//定义一组任务
private List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
/**
* 普通任务
*/
@XxlJob("firstJob")
public void firstJob() throws Exception {
System.out.println("firstJob执行了.... " + LocalDateTime.now());
for (Integer data : dataList) {
XxlJobHelper.log("data= {}", data); //写日志到xxl-job中
//每次执行任务后,随机暂停一段时间
Thread.sleep(RandomUtil.randomInt(100, 500));
}
System.out.println("firstJob执行结束了.... " + LocalDateTime.now());
}
}
编写完成后,重启服务,在调度中心中创建任务:
启动任务,看效果:
4.3.3.3. cron表达式
在上述的任务中,采用的是固定速率执行,实际上这种方式并不灵活,比如我们想每天的00:10:00执行任务,显然固定速率是不能满足需求的。
在我们使用调度任务技术的时候,特别是调度框架,里面都支持使用日历的方式来设置任务制定的时间、频率等,通常情况下都会使用cron表达式来表达
cron表达式是一个字符串, 类似这样:*/5 * * * * ?
,用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:
组成部分 | 含义 | 取值范围 |
第一部分 | Seconds (秒) | 0-59 |
第二部分 | Minutes(分) | 0-59 |
第三部分 | Hours(时) | 0-23 |
第四部分 | Day-of-Month(天) | 1-31 |
第五部分 | Month(月) | 0-11或JAN-DEC |
第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
第七部分 | Year(年) 可选 | 1970-2099 |
另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
符号 | 含义 |
? | 表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?” |
* | 代表所有可能的值 |
, | 设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务 |
- | 设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务 |
/ | 设置频率或间隔,如"1/15"表示从1分开始,每隔15分钟运行一次任务 |
L | 用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如"6L"表示"每月的最后一个星期五" |
W | 表示离给定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日" |
# | 表示该月第几个周X。例如”6#3”表示该月第3个周五 |
为了让大家更熟悉cron表达式的用法, 接下来我们给大家列举了一些例子, 如下表所示:
cron表达式 | 含义 |
*/5 * * * * ? | 每隔5秒运行一次任务 |
0 0 23 * * ? | 每天23点运行一次任务 |
0 0 1 1 * ? | 每月1号凌晨1点运行一次任务 |
0 0 23 L * ? | 每月最后一天23点运行一次任务 |
0 26,29,33 * * * ? | 在26分、29分、33分运行一次任务 |
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时运行一次任务 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15运行一次任务 |
修改任务为cron表达式:
1.3.3.4. 分片式任务
如果我们将zzyl-xxl-job
服务启动2个,当任务触发执行时,该执行哪个呢?这里就涉及到路由策略了:
可以看到,路由策略有很多,在上述场景中,我们适合设定【轮询】策略,意思是,这次调度执行节点1,下次执行节点2,再下次执行节点1,……
再想一下,我们启动了2个服务节点,可每次只能执行一个,另一个处理休息状态,这不是很浪费资源吗?有没有办法可以让两个都执行?
如果两个节点都执行了,那每个节点处理的任务都是一样的,这不就重复处理了吗?
有没有办法做到,2个服务节点同时执行,但是处理的任务又不重复呢?可以的,那就是【分片广播】路由策略。图示如下:
代码实现:
/**
* 分片式任务
*/
@XxlJob("shardingJob")
public void shardingJob() throws Exception {
// 分片节点总数
int shardTotal = XxlJobHelper.getShardTotal();
// 当前节点下标,从0开始
int shardIndex = XxlJobHelper.getShardIndex();
System.out.println("shardingJob执行了.... " + LocalDateTime.now());
for (Integer data : dataList) {
if (data % shardTotal == shardIndex) {
System.out.println("data= " + data);
XxlJobHelper.log("data= {}", data);
Thread.sleep(RandomUtil.randomInt(100, 500));
}
}
System.out.println("shardingJob执行结束了.... " + LocalDateTime.now());
}
创建任务:
启动2个服务节点:
测试执行:
可以看到,两个服务节点同时执行,并且每个节点执行的任务内容是不同的,两个节点共同完成的任务的处理,也就是加快了任务处理的速度。
分片式任务调度可用于,待处理的任务较多,需要增加服务节点来并行执行任务的场景,例如:需要在1小时内发出1亿条短信。
4.3.4. xxl-job 任务详解
4.3.4.1. 执行器
执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能;
另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器
以下是执行器的属性说明:
属性名称 | 说明 |
AppName | 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用; |
名称 | 执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性; |
排序 | 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表; |
注册方式 | 调度中心获取执行器地址的方式; |
机器地址 | 注册方式为"手动录入"时有效,支持人工维护执行器的地址信息; |
自动注册和手动注册的区别和配置:
1.3.4.2. 基础配置
在我们新建任务的时候,里面有很多的配置项,下面我们就来介绍下里面具体的作用
基础配置
- 执行器:每个任务必须绑定一个执行器, 方便给任务进行分组
- 任务描述:任务的描述信息,便于任务管理;
- 负责人:任务的负责人;
- 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔
调度配置
- 调度类型:
-
- 无:该类型不会主动触发调度;
-
- CRON:该类型将会通过CRON,触发任务调度;
-
- 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发;
任务配置
- 运行模式:BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
- JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
- 执行参数:任务执行所需的参数;
阻塞处理策略
阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
- 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO(First Input First Output)队列并以串行方式运行;
- 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
- 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
路由策略
当执行器集群部署时,提供丰富的路由策略,包括:
- FIRST(第一个):固定选择第一个机器;
- LAST(最后一个):固定选择最后一个机器;
- ROUND(轮询)
- RANDOM(随机):随机选择在线的机器;
- CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
- LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
- LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
- FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
- BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
- SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
1.3.5. 调度流程
1.4. 预约管理定时修改状态
依据我们之前分析的需求,当用户没有按时来访之后,会把预约的状态修改为已过期
我们可以每隔半个小时查询一次数据,如果状态是未到访并且已经过了预约时间,则设置为已过期
1.4.1. 项目中集成xxl-job
(1)创建执行器和任务
登录调度中心后,手动创建执行器和任务
执行器,一般一个项目只需要创建一个执行器即可,在执行器下可以创建多个任务
创建任务
(2)项目中集成
模块名:zzyl-framework 核心技术模块:
在application.yml文件中的配置也要拷贝过来,修改为自己定义的执行器:
xxl:
job:
admin:
addresses: http://192.168.200.146:8888/xxl-job-admin
executor:
appname: zzyl-dev-executor
port: 9999
ip: 192.168.200.1
4.4.2. 定时任务代码编写
在ReservationMapper文件中新增一个方法(根据时间修改状态),用于修改预约的状态
void updateReservationStatus(@Param("minusDays")LocalDateTime minusDays);
对应的xml文件:
<update id="updateReservationStatus" parameterType="java.time.LocalDateTime">
UPDATE reservation
SET status = 3
WHERE status = 0 AND time < #{minusDays}
</update>
在ReservationService业务层代码中新增一个方法(根据时间修改状态),用于修改预约的状态
/**
* 过期状态修改
* @param now
*/
void updateReservationStatus(LocalDateTime now);
实现类:
/**
* 过期状态修改
* @param now
*/
@Override
public void updateReservationStatus(LocalDateTime now) {
reservationMapper.updateReservationStatus(now);
}
在zzyl-service
模块下创建任务类:
package com.zzyl.job;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.zzyl.service.ReservationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 预约管理定时修改状态
*/
@Slf4j
@Component
public class ReservationJob {
@Resource
private ReservationService reservationService;
@XxlJob("reservationStatusToExpired")
public void updateReservationStatus() {
log.info("预约状态-过期修改-begin");
this.reservationService.updateReservationStatus(LocalDateTime.now());
log.info("预约状态-过期修改-end");
}
}
启动项目测试,在测试的时候,大家可以先把cron表达式修改为1分钟执行一次,手动在数据库改一些数据
总结xxl-job
1.分布式调度在项目中的应用场景
- 定时修改支付状态
- 定时匹配运输任务
- 定时重新发送失败的消息
2.xxljob是如何通信的
- xxljob 服务端 与 客户端通信 是通过netty搭建的通信服务器,基于的协议是http协议
- 所以当我们的微服务配置的xxljob执行器 需要单独设置端口号
3.xxljob具体如何使用
- 执行器根据配置的调度中心的地址,自动注册到调度中心
- 达到任务触发条件,调度中心下发任务
- 执行器基于线程池执行任务,并把执行结果放入内存队列中,把执行日志写在日志文件中
- 执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心
- 当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行去读取任务日志并返回日志详情
4.xxljob的工作原理
- XXL-Job 的工作原理主要分为两个部分:调度中心和执行器。
- 调度中心负责任务的管理、调度和监控,执行器负责任务的具体执行。
- 具体来说,XXL-Job 的工作流程如下:
- 开发人员在 XXL-Job 的管理界面上创建任务,并设置定时规则和执行器信息等相关参数。
- 调度中心将任务信息保存在数据库中,并生成执行计划。当到达任务的执行时间时,调度中心会根据任务的执行计划选择一个合适的执行器进行任务的执行,同时记录任务的执行情况。
- 执行器在执行任务前会向调度中心获取任务信息,并执行具体的任务逻辑。任务执行完毕后,执行器会将执行结果上传到调度中心,并更新任务的状态。
- 如果任务执行失败,调度中心会记录失败信息,并根据任务的重试次数和重试间隔重新调度任务。
- 调度中心提供了丰富的 Web 界面和 API接口,可以方便地查询和管理任务的执行情况。
- 总之,XXL~Job 通过调度中心和执行器协作的方式,实现了对任务的灵活调度、管理和监控,保证了任务的准确性和可靠性。
5.xxljob 任务触发时,路由策略
- 第一台机器:任务只会被调度到第一台注册到调度中心的执行器上。
- 最后一台机器:任务只会被调度到最后一台注册到调度中心的执行器上。
- 轮询:任务将按照顺序依次调度到每台注册到调度中心的执行器上
- 随机:任务会随机调度到注册到调度中心的任意一个执行器上。
- 故障转移(失效转移):当一个执行器不可用时,任务会自动转移到下一个可用的执行器上
6.xxljob使用的版本号
- xxljob不同间会有版本兼容性问题,看你有没有关注这一点
- 我们使用的是 2.3.0版本