Quartz 从定时到爆炸

原理:前人之述备矣

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注解了, 使用相当简单

  1. 准备一个集成了springboot项目

  2. 在启动类上打上注解 , 开启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);
    }
}
  1. 准备周期任务类 , 执行周期任务

    需要交给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 优势
  1. 持久化定时任务

    Quartz提供了系列配置,配置后可以将定时任务保存到数据库, 当服务器宕机重启后, 保证宕机期间的定时任务在重启后也能执行

  2. 管理定时任务

    Scheduled在使用时,定时任务执行周期由后台服务器的代码决定好了且无法更改,但是Quartz可以从自定义周期/时间来执行定时任务, 例如, 用户自定义商品上架时间, 订单过期时间修改 , 更加"解耦"

3.2 Quartz使用

​ 跳过基本项目搭建

  1. 引入maven依赖
<!--quartz定时任务-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  1. 持久化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
  1. 引入工具类,定时任务实例

    该项目中我做了简单的业务 : 在业务类中添加一条数据, 设置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);
        }
    }

}
  1. 制作定时任务业务类 , 也就是定时任务需要处理的业务

    集成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);
    }
}
  1. 编写控制层接收请求
@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";
    }

}
  1. 编写业务层处理业务
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);
    }
}
  1. 测试定时任务

    1. 使用浏览器调用接口 localhost:8089/quartz/setJob/10/我是一枝花
    2. 查看数据库数据 (id, 创建时间,理应执行时间,内容, 实际执行时间,执行是否成功)

    在这里插入图片描述

    1. 等待10秒后,再次查看数据库

    在这里插入图片描述

    1. 测试成功
3.3 Quartz持久化属性
  1. 先随便写个周期任务

    控制层

@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+"--------");
    }
}
  1. 重新启动服务器 , 并在浏览器运行localhost:8089/quartz/cycleJob

    控制台开始打印,周期任务创建成功

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZfLTbcXo-1628142838284)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210805112956189.png)]

  2. 查看数据库表

    1. 在 QRTZ_CRON_TRIGGERS 表中查看信息 (调度器名,触发器名,触发器组名,cron表达式,时区)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hOjBsYG6-1628142838285)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210805113517148.png)]

    1. 其他表就不解读了, 只要存在这些信息则表示持久化成功了的
3.4 删除定时任务

​ 当任务过期 , 或者任务失效后, 需要删除任务 , 在业务中只要表记录了任务名, 可以调用方法进行删除, 这里小小实现一下

  1. 控制层新增接口, 根据任务名删除任务,此项目中没有保存任务名,使用的 UUID , 则去QRTZ_JOB_DETAILS表中查找任务名
@GetMapping("/deleteJob/{jobName}")
public void delete(@PathVariable String jobName){
    QuartzUtils.removeJob(jobName);
}
  1. 打开浏览器访问 localhost:8089/quartz/deleteJob/e8846e0a-a388-43c8-aeda-47b3d5c17880
  2. 访问Quartz持久化的数据库表中,发现记录被清楚, 且 控制台不再打印语句 , 则删除成功
3.5 学习历程
  1. 关于定时器和消息队列

    1. 对于只执行一次的定时任务 , 更推荐使用消息队列, 消息队列所承受的并发量更高

    例如下单后未支付关闭订单, 就使用消息队列, 商城的并发量高,若使用定时器会对服务器造成很大压力,并且持久化后 , 服务器还会随时扫描表中的定时任务, 满足条件就会执行 , 但很多扫描都是无效的, 却浪费了资源的开销

    1. RocketMQ 默认有18个延迟队列 , 对应18个等级 , 在发送消息的时候进行设置 , 较为灵活, 但是设置的最高的延迟时间只有2h, 超过2h后需要多次发送 . 延迟消息发送后会保存在broker , 等到时间到了会向消费者推送进行消费

    2. RabbitMQ 需要在服务器启动时自己配置延迟队列 , 设置后才能使用 , 因此需要启动时就定义好所需的延迟队列, 不太灵活 . 其原理是延迟队列并没有消费者进行消费, 等到达规定的时间后, 会将消息丢向死信队列 , 而死信队列有相应的消费者进行消费

    3. 又对于像闹钟这种业务 , 需要在一个确定的时间点, 而且延迟时间可能很长, 一个月后, 这种情况下就只能使用定时器了 , 根据选择的日期生成相应的cron表达式 , 调度器会在相应的时间节点开启任务

    4. 总之 , 使用什么工具需要根据业务进行选择 , 工具是死的, 人是活的

  2. 关于定时器执行的时间偏移

    在我所测试的数据中 , 定时器执行的任务时间不够精准 , 最大的偏移量是3.934秒 , 因此针对业务需要合理选择

    理应执行时间 ( 应定位到每分的000毫秒 ) , 内容 , 定时器执行时间 , 是否执行成功
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值