原理:前人之述备矣
1. cron表达式
cron表达式一共七位 , 意义分别为:秒,分,时,日,月,周,年 , 其中第七位代表年份可以省略
特殊符号:
*: 意为都 , 在哪一位上代表每个时间点都会执行
?: 只能在日和周中使用,意为不指定日或周,在日固定的情况下周必须用?, 反之亦然
/: 从其左边的数字开始, 每隔右边的数字执行一次 , 例如 第一位上 0/10, 则表示从每分的0秒开始,每隔10秒执行一次
-: 表示周期中每个时间段执行, 例如第一位上 0-10 ,则表示每分的0,1,2…10秒分别执行一次
L: 以为last , 在某个字段上表示该字段的最后一个时间点执行, 例如 第一位上为 L , 则在59秒执行
2. Spring 自带定时任务 Scheduled
2.1 Scheduled 集成
引入Spring的包后就能使用Scheduled注解了, 使用相当简单
-
准备一个集成了springboot项目
-
在启动类上打上注解 , 开启Scheduled
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling //开启Scheduled
public class QuartzDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzDemoApplication.class);
}
}
-
准备周期任务类 , 执行周期任务
需要交给spring管理 ,并且在业务方法上打上注解@Scheduled
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ScheduledTimingTask {
@Scheduled(fixedRate = 10000)
public void test1(){
System.out.println(LocalDateTime.now()+": 定时器启动");
}
@Scheduled(cron = "0/10 * * * * ?")
public void test2(){
System.out.println(LocalDateTime.now()+": 定时器启动");
}
}
2.2 注解@Scheduled
//使用cron表达式进行的定时任务
String cron() default "";
//时区,默认空字符串为服务器时区
String zone() default "";
//自上一个定时任务结束后开始计时,到下一个定时任务开始的间隔时间
long fixedDelay() default -1;
//与fixedDelay类似,但使用的是String类型,并且可以使用占位符,从配置文件中获取
//@Scheduled(fixedDelayString="${delay.orderwait}")
String fixedDelayString() default "";
//自上一个定时任务开始时进入计时,到下一个定时任务开始的间隔时间
long fixedRate() default -1;
//与fixedRate类似,但使用的是String类型,并且可以使用占位符,从配置文件中获取
//@Scheduled(fixedRateString="${rate.order.wait}")
String fixedRateString() default "";
//在第一次任务执行前延迟的时间,可以与上述的除了cron的共同配置生效
long initialDelay() default -1;
//与initialDelay类似,但使用的是String类型,并且可以使用占位符,从配置文件中获取
String initialDelayString() default "";
3. Quartz初体验
3.1 优势
-
持久化定时任务
Quartz提供了系列配置,配置后可以将定时任务保存到数据库, 当服务器宕机重启后, 保证宕机期间的定时任务在重启后也能执行
-
管理定时任务
Scheduled在使用时,定时任务执行周期由后台服务器的代码决定好了且无法更改,但是Quartz可以从自定义周期/时间来执行定时任务, 例如, 用户自定义商品上架时间, 订单过期时间修改 , 更加"解耦"
3.2 Quartz使用
跳过基本项目搭建
- 引入maven依赖
<!--quartz定时任务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
-
持久化Quartz
首先需要将quartz持久化的表导入到数据库中, 然后再在下面的yml配置中指向该表
spring:
quartz:
#持久化到数据库
job-store-type: jdbc
properties:
org:
quartz:
datasource:
driver-class-name: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql:///quartz-demo?characterEncoding=UTF-8
username: root
password:
scheduler:
instancName: clusteredScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
#StdJDBCDelegate说明支持集群
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 1000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 20
threadPriority: 5
-
引入工具类,定时任务实例
该项目中我做了简单的业务 : 在业务类中添加一条数据, 设置id作为定时器参数,在定时任务中修改状态
QuartzJob 实例类
import lombok.Data;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@Data
public class QuartzJob {
//任务名
private String jobName;
//传递的参数
private Map<String, Object> params;
//时间表达式
private String cron;
//时间对象
private Date date;
//定时任务所用的cron表达式,周期任务不适用
public void setDate(Date date) {
this.date = date;
String[] cronArr = new String[7];
for (int i = 0; i < cronArr.length; i++) {
cronArr[i] = "";
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int second = calendar.get(Calendar.SECOND);
int minute = calendar.get(Calendar.MINUTE);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int month = calendar.get(Calendar.MONTH) + 1;
int year = calendar.get(Calendar.YEAR);
cronArr[0] = second + "";
cronArr[1] = minute + "";
cronArr[2] = hour + "";
cronArr[3] = day + "";
cronArr[4] = month + "";
cronArr[5] = "?";
cronArr[6] = year + "";
String cron = StringUtils.join(cronArr, " ").trim();
this.setCron(cron);
}
}
Quartz工具类
import cn.qiuming.quartz.QuartzDemoApplication;
import cn.qiuming.quartz.quartz.entity.QuartzJob;
import org.quartz.*;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
/**
* Quartz调度管理器
*/
public class QuartzUtils {
private static final String JOB_GROUP_NAME = "JOB_GROUP_SYSTEM";
private static final String TRIGGER_GROUP_NAME = "TRIGGER_GROUP_SYSTEM";
//调度器 将启动类的springapplication.run()方法的返回值提取成静态变量,即可调用
private static Scheduler sched = QuartzDemoApplication.context.getBean(Scheduler.class);
/**
* 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
* @param jobName 任务名
* @param cls 任务字节码
* @param params 任务参数
* @param time 时间设置,参考quartz说明文档
*/
public static void addJob(String jobName, Class<? extends Job> cls,
Object params, String time) {
try {
JobKey jobKey = new JobKey(jobName, JOB_GROUP_NAME);// 任务名,任务组,任务执行类
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("params", params);
JobDetail jobDetail = newJob(cls).withIdentity(jobKey).setJobData(jobDataMap).build();
TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);// 触发器
System.out.println(time);
Trigger trigger = newTrigger().withIdentity(triggerKey).withSchedule(cronSchedule(time)).build();// 触发器时间设定
sched.scheduleJob(jobDetail, trigger);
if (!sched.isShutdown()) {
sched.start();// 启动
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void addJob(Class<? extends Job> cls, QuartzJob quartzJob) {
addJob(quartzJob.getJobName(),cls,quartzJob.getParams(),quartzJob.getCron());
}
/**
* 修改一个任务的触发时间
* @param triggerName 触发器名 默认创建时和人物名一致
* @param time 触发时间
*/
public static void modifyJobTime(String triggerName, String time) {
try {
TriggerKey triggerKey = new TriggerKey(triggerName, JOB_GROUP_NAME);
CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(time)) {
// 修改时间
trigger.getTriggerBuilder().withSchedule(cronSchedule(time));
// 重启触发器
sched.resumeTrigger(triggerKey);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 移除一个任务
* @param jobName 任务名
*/
public static void removeJob(String jobName) {
try {
TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);
//停止触发器
sched.pauseTrigger(triggerKey);
//移除触发器
sched.unscheduleJob(triggerKey);
JobKey jobKey = new JobKey(jobName, JOB_GROUP_NAME);
//删除任务
sched.deleteJob(jobKey);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
-
制作定时任务业务类 , 也就是定时任务需要处理的业务
集成QuartzJobBean抽象类,复写executeInternal方法 , TimingTask是我业务中的实例类, 需要自行修改方法
import cn.qiuming.quartz.domain.TimingTask;
import cn.qiuming.quartz.mapper.TimingTaskMapper;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.time.LocalDateTime;
import java.util.Map;
public class TimingJobDemo extends QuartzJobBean {
@Autowired
private TimingTaskMapper timingTaskMapper;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//获取参数
JobDataMap dataMap = jobExecutionContext.getMergedJobDataMap();
Map map = (Map<String,Object>) dataMap.get("params");
Long id = ((Long) map.get("id"));
TimingTask timingTask = timingTaskMapper.selectById(id);
//设置任务执行时间
timingTask.setExecutionTime(LocalDateTime.now().toString());
//设置任务执行成功
timingTask.setSuccess(true);
timingTaskMapper.updateById(timingTask);
}
}
- 编写控制层接收请求
@RequestMapping("/quartz")
@RestController
public class QuartzController {
@Autowired
private QuartzService quartzService;
@GetMapping("/setJob/{time}/{content}")
public String setJob(@PathVariable String time, @PathVariable String content) {
quartzService.setJob(time, content);
return "success";
}
}
- 编写业务层处理业务
public interface QuartzService extends IService<TimingTask> {
void setJob(String time, String content);
}
import cn.qiuming.quartz.domain.TimingTask;
import cn.qiuming.quartz.mapper.TimingTaskMapper;
import cn.qiuming.quartz.quartz.entity.QuartzJob;
import cn.qiuming.quartz.quartz.job.CycleJobDemo;
import cn.qiuming.quartz.quartz.job.TimingJobDemo;
import cn.qiuming.quartz.quartz.util.QuartzUtils;
import cn.qiuming.quartz.service.QuartzService;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class QuartzServiceImpl extends ServiceImpl<TimingTaskMapper, TimingTask> implements QuartzService {
@Override
public void setJob(String time, String content) {
Long second = Long.valueOf(time);
TimingTask task = new TimingTask();
//设置开始时间
task.setCreateTime(LocalDateTime.now().toString());
//设置类容
task.setTaskInfo(content);
//设置理应启动时间
task.setStartTime(LocalDateTime.now().plusSeconds(second).toString());
//将消息保存到数据库
baseMapper.insert(task);
//封装quartz_job
QuartzJob quartzJob = new QuartzJob();
//设置定时任务名称
quartzJob.setJobName(UUID.randomUUID().toString());
long l = System.currentTimeMillis() + second * 1000;
//将定好的时间转为cron表达式 , 详情看QuartzJob.setDate()方法
quartzJob.setDate(new Date(l));
//设置定时任务参数
HashMap<String, Object> map = new HashMap<>();
map.put("id", task.getId());
quartzJob.setParams(map);
//提交quartz任务
QuartzUtils.addJob(TimingJobDemo.class, quartzJob);
}
}
-
测试定时任务
- 使用浏览器调用接口
localhost:8089/quartz/setJob/10/我是一枝花
- 查看数据库数据 (id, 创建时间,理应执行时间,内容, 实际执行时间,执行是否成功)
- 等待10秒后,再次查看数据库
- 测试成功
- 使用浏览器调用接口
3.3 Quartz持久化属性
-
先随便写个周期任务
控制层
@GetMapping("/cycleJob")
public void cycleJob() {
QuartzJob quartzJob = new QuartzJob();
quartzJob.setJobName(UUID.randomUUID().toString());
Map<String, Object> map = new HashMap<>();
map.put("content", "时日至今空方明,仅次余生愿躺平");
quartzJob.setParams(map);
//每5秒执行一次
quartzJob.setCron("0/5 * * * * ?");
QuartzUtils.addJob(CycleJobDemo.class, quartzJob);
}
定时任务业务类
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.Map;
public class CycleJobDemo extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Map<String, Object> map = (Map<String, Object>) jobExecutionContext.getMergedJobDataMap().get("params");
Object content = map.get("content");
System.out.println(content+"--------");
}
}
-
重新启动服务器 , 并在浏览器运行
localhost:8089/quartz/cycleJob
控制台开始打印,周期任务创建成功
-
查看数据库表
- 在 QRTZ_CRON_TRIGGERS 表中查看信息 (调度器名,触发器名,触发器组名,cron表达式,时区)
- 其他表就不解读了, 只要存在这些信息则表示持久化成功了的
3.4 删除定时任务
当任务过期 , 或者任务失效后, 需要删除任务 , 在业务中只要表记录了任务名, 可以调用方法进行删除, 这里小小实现一下
- 控制层新增接口, 根据任务名删除任务,此项目中没有保存任务名,使用的 UUID , 则去QRTZ_JOB_DETAILS表中查找任务名
@GetMapping("/deleteJob/{jobName}")
public void delete(@PathVariable String jobName){
QuartzUtils.removeJob(jobName);
}
- 打开浏览器访问
localhost:8089/quartz/deleteJob/e8846e0a-a388-43c8-aeda-47b3d5c17880
- 访问Quartz持久化的数据库表中,发现记录被清楚, 且 控制台不再打印语句 , 则删除成功
3.5 学习历程
-
关于定时器和消息队列
- 对于只执行一次的定时任务 , 更推荐使用消息队列, 消息队列所承受的并发量更高
例如下单后未支付关闭订单, 就使用消息队列, 商城的并发量高,若使用定时器会对服务器造成很大压力,并且持久化后 , 服务器还会随时扫描表中的定时任务, 满足条件就会执行 , 但很多扫描都是无效的, 却浪费了资源的开销
-
RocketMQ 默认有18个延迟队列 , 对应18个等级 , 在发送消息的时候进行设置 , 较为灵活, 但是设置的最高的延迟时间只有2h, 超过2h后需要多次发送 . 延迟消息发送后会保存在broker , 等到时间到了会向消费者推送进行消费
-
RabbitMQ 需要在服务器启动时自己配置延迟队列 , 设置后才能使用 , 因此需要启动时就定义好所需的延迟队列, 不太灵活 . 其原理是延迟队列并没有消费者进行消费, 等到达规定的时间后, 会将消息丢向死信队列 , 而死信队列有相应的消费者进行消费
-
又对于像闹钟这种业务 , 需要在一个确定的时间点, 而且延迟时间可能很长, 一个月后, 这种情况下就只能使用定时器了 , 根据选择的日期生成相应的cron表达式 , 调度器会在相应的时间节点开启任务
-
总之 , 使用什么工具需要根据业务进行选择 , 工具是死的, 人是活的
-
关于定时器执行的时间偏移
在我所测试的数据中 , 定时器执行的任务时间不够精准 , 最大的偏移量是3.934秒 , 因此针对业务需要合理选择
理应执行时间 ( 应定位到每分的000毫秒 ) , 内容 , 定时器执行时间 , 是否执行成功