深入浅出,SpringBoot整合Quartz实现定时任务与Redis健康检测(二)

前言

在上一篇深入浅出,SpringBoot整合Quartz实现定时任务与Redis健康检测(一)_往事如烟隔多年的博客-CSDN博客

文章中对SpringBoot整合Quartz做了初步的介绍以及提供了一个基本的使用例子,因为实际各自的需求任务不尽相同因此并未对定时任务的代码做相关填充。本文将对Redis的健康检测进行进一步的实现,并且将尝试逐步缩减相关代码,一步步优化定时任务的创建流程。

Redis定时检测

由于项目本身是一个学习类型项目,因此实际的使用数据量并不会特别大。当然引入Redis是考虑到在对热点数据重复访问时提升响应速度,关于缓存的实现方式多种多样,此处就以Redis为例。

问题复现

在引入Redis之后,某些接口在使用时需要从Redis中存取数据,就需要保证Redis的存活状态,否则会抛出异常。而如果在项目使用之初并没有配置Redis也将直接导致项目无法启动。此外在长时间不使用Redis时其连接池资源被释也会导致第一次访问时数据非常缓慢,以上的情景在使用时是非常糟糕的。

解决方案

在一些实际的开发场景中,遇到如上情况时可能会使用Hystrix来进行熔断,或者采用限流等措施。然而对于我们来讲,能够清楚的预见访问流量的有限性,即使只使用单机的数据库也能支撑服务,那么能否在Redis不可用时切换到MySQL查询呢?答案是可以的。

这里使用Redis中的ping命令来实现对Redis存活状态的检测,由于本项目中对于字符串类操作较多,这里工具使用了StringRedisTemplate进行封装。RedisOperator即为其它类中操作Redis的工具类,这里将其交由Spring容器管理。

@Component
public class RedisOperator {

	@Autowired
	private StringRedisTemplate redisTemplate;


	/**
	 * 07.19 新增ping,用于redis健康检测,连接失败的异常将不会返回给前端,也不会进入全局异常处理
	 * @return
	 */
	public boolean ping(){
		boolean result;
		try{
			result = "PONG".equals(redisTemplate.getConnectionFactory().getConnection().ping());
		}catch (RedisConnectionFailureException e){
			log.error("捕获Redis连接异常,请检查服务运行状态!");
			result = false;
		}
		return result;
	}
    
    // 其它命令...


}

上述代码通过使用ping命令的返回值判断Redis当前的存活状态,那么实际的使用场景如何呢?以下是一个简单例子:

可以看到如果不进行检测的话,此时Redis处于未连接状态时将造成阻塞,该方法会一直等待判断key是否存在解决状态的返回,最终达到超时时间后将抛出异常,体验非常糟糕。 

if (redisOperator.keyIsExist(shopCategoryCacheKey.toString())) {
                    // 获取数据返回
                    shopCategoryVOList = cacheOperator.readCache(shopCategoryCacheKey.toString(),
                            new ArrayList<ShopCategoryVO>());
                    return new PageVO(shopCategoryVOList, shopCategoryVOList.size());
}

那么如何检测呢?如上述代码不在少数,也就是说每次执行时都要发送ping命令执行验证Redis存活状态,而Redis只有在连接状态下才会及时返回,除此之外均需等待到超时结束返回异常。这里就可以用到前一篇文章的定时任务了,我们可以通过将ping命令执行交给定时任务进行检测,然后维持一个boolean类型的标识,每次判别标识即可。当Redis处于未连接状态时自然不会执行上述代码,进而执行数据库查询操作。

定时任务

由于上一篇文章中已经编写了配置类

@Configuration
@Slf4j
public class RedisCheckConfig {
    // 指定生成的Bean实例对象名称
    @Bean("redisCheck")
    public JobDetail jobDetail() {
        return JobBuilder.newJob(RedisCheckJob.class)
                // 任务名和任务分组
                .withIdentity("RedisCheckJob", "group")
                .withDescription("任务描述:内存方式运行")
                .storeDurably()
                .build();
    }
 
    @Bean("redisTrigger")
    public Trigger trigger() {
        return TriggerBuilder.newTrigger()
                // 触发器名称和分组
                .withIdentity("redisCheck", "group")
                .forJob(jobDetail())
                .startNow()
                // 使用SimpleSchedule构建定时任务
                .withSchedule(
                        SimpleScheduleBuilder
                                .simpleSchedule()
                                 // 每隔10s执行任务
                                .withIntervalInSeconds(10)
                                // 永不过期
                                .repeatForever())
                .build();
    }
}

因此这里只需要编写实际的业务类即可,可以看到这里通过依赖注入获取到RedisOperator类,它用于获取ping命令执行后的结果。

@Slf4j
public class RedisCheckJob extends QuartzJobBean {
    
    @Autowired
    private RedisOperator redisOperator;
    
    /**
    * Redis连接状态标识
    */
    public static boolean redisConnected;
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
         log.info("开始检测Redis连接状态");
         redisConnected = redisOperator.ping();
         log.info("Redis当前是否连接 "+ redisConnected);
    }
}

到这里就需要在进行Redis操作的代码前加入如下判断即可,这样就实现了Redis的一样定期保活和健康检测,一旦Redis处于未连接状态的将直接调用数据库查询,而定时器执行检测的间隔将由使用者自行设置。

// 判断Redis存活状态
if(RedisCheckJob.redisConnected){
    // Redis操作    
    if (redisOperator.keyIsExist(shopCategoryCacheKey.toString())) {
        // ....
    }
}

优化

虽然前文中已经对完成了文章开头所需要的解决的问题,但每一次创建新的定时任务时均需要编写JobDetail和Trigger代码,似乎有些冗余,能否对其对进一步优化呢? 实际上对于普通的定时任务使用上述操作即可,此处为个人学习探究内容,酌情观看。

抽象类 VS 接口

考虑到需要构建JobDetail和Trigger均需要使用name和group属性,因此考虑使用一个类进行管理,在SpringBoot中使用@Configuration和@Bean注解可以将定时任务配置类关联到Scheduler中,而若手动创建对象时则要考虑如何如何创建配置类?获取配置类?如何调用的问题?

在上一篇文中提到可以通过继承 QuartzJobBean 类并重写其excuteInternal方法,或实现 Job 接口的excute方法,从QuartzJobBean的源码可知,其实现了Job接口,因此以上的创建方式任选其一即可。  

这里就涉及到一个选择性的问题,最终编写的实现类应该具有任务名、分组名称、触发器这三个属性,它们将用于后续任务的构建。由于需要获取这三个属性,因此考虑使用抽象方法获取,因为最终的定时任务类为普通类,只负责信息的初始化,而目前来看若无需属性控制且均为抽象方法时,则可以将该类转为接口,这里无论选择抽象类还是接口,都需要确保最终实现了Job接口。

这里以接口为例构建,scheduleBuilder()方法用于接收实现类的定时器,通过实现该方法将由子类中自行决定使用哪种定时器。

public interface IntervalJobInterface extends Job {
    /**
     * 任务名称
     * @return
     */
    String jobName();
    /**
     * 任务分组
     * @return
     */
    String jobGroup();
    /**
     * 不同的任务定时器,由实现类构建
     * @return
     */
    ScheduleBuilder scheduleBuilder();
}

编写定时任务类如下

@Slf4j
@Component
public class RedisCheckConfigForInterface implements IntervalJobInterface {

    @Autowired
    private RedisOperator redisOperator;

    /**
     * 任务名称
     */
    public String name = "redis-check";

    /**
     * 分组名称
     */
    public String group = "redis";

    /**
     * 任务执行周期,单位s
     */
    public Integer intervalTime = 60;

    /**
     * redis连接状态
     */
    public static boolean redisConnected;


    @Override
    public String jobName() {
        return name;
    }

    @Override
    public String jobGroup() {
        return group;
    }

    /**
     * 不同定时器
     *
     * @return
     */
    @Override
    public ScheduleBuilder scheduleBuilder() {
        return SimpleScheduleBuilder
                .simpleSchedule()
                .withIntervalInSeconds(intervalTime)
                // 永不过期
                .repeatForever();
    }

    
    /**
     * 定时任务逻辑
     *
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("开始检测Redis连接状态,IntervalInterface");
            redisConnected = redisOperator.ping();
            log.info("Redis当前是否连接 "+ redisConnected);
    }
}

之后可以通过@Component或@Confiugration注解将其放入Spring的容器中,方便取用。 

@Component VS @Confiugration

如下为获取配置类信息的代码,其作用为获取 IntervalJobInterface 接口的所有实现类,然后通过实例获取其任务名和分组名,上一步中提到实现类的编写可以使用@Component或@Confiugration注解,那两者有什么区别呢?一般来说@Configuration注解与@Bean注解作用于配置类上,但目前代码中没有使用@Bean注解生成Bean实例,那么是否可以在任务类上直接使用@Configuration注解呢?

这里需要特别注意,若需要在其它地方通过反射机制获取如上任务类的属性时,@Configuration标注的类将使用cglib生成代理类,反射获取不能直接取得类信息获得属性,需要通过getClass().getSuperClass()的方式获取。而@Component注解作用的类生成的原有的类实例,可以直接getClass()获取类信息,再获取属性。

既然可以直接反射获取其相关属性,为什么还需要添加一个接口?由于反射获取的属性需要创建新对象重新组装,JobDetail和Trigger都需要一个实例类信息,添加一个接口可以获取信息的同时也能用作实例类的描述,详见如下代码:

@Component
@Slf4j
public class QuartzConfig {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private Scheduler scheduler;

    /**
     * Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
     * 获取所有实现了任务标记接口类,
     */
    @PostConstruct
    public void getInitBeans() {
        log.info("开始获取定时任务");
        Map<String, IntervalJobInterface> intervalJobInterfaceMap = applicationContext.getBeansOfType(IntervalJobInterface.class);
        intervalJobInterfaceMap.forEach((className, jobInstance) -> {

            IntervalJobInterface intervalJobs = (IntervalJobInterface) jobInstance;
            log.info("任务名称: " + intervalJobs.jobName());
            log.info("任务分组: " + intervalJobs.jobGroup());

            // 定时任务不存在,无法执行
            if (intervalJobs.scheduleBuilder() == null) {
                log.error(className + " 任务无法运行, 请指定任务的运行周期时间后再试!");
                return;
            }

            JobDetail jobDetail = jobDetail(intervalJobs);
            Trigger trigger = trigger(jobDetail, intervalJobs);

            try {
                scheduler.scheduleJob(jobDetail, trigger);
                // crontab 表达式的任务不会立即执行,如需立即执行则取消如下条件判断代码的注释
                //if (intervalJobs.scheduleBuilder() instanceof CronScheduleBuilder) {
                //    scheduler.triggerJob(jobDetail.getKey());
                //}

                log.info("已添加 " + intervalJobs.jobName() + " 任务 " + " jobKey" + jobDetail.getKey());
            } catch (SchedulerException e) {
                log.error("定时任务出现异常");
                e.printStackTrace();
            }

        });
        log.info("获取定时任务结束");
    }

    /**
     * 任务详情
     *
     * @param intervalJobs
     * @return
     */
    public JobDetail jobDetail(IntervalJobInterface intervalJobs) {
        return JobBuilder.newJob(intervalJobs.getClass())
                .withIdentity(intervalJobs.jobName(), intervalJobs.jobGroup())
                .withDescription("内存运行")
                .storeDurably()
                .build();
    }

    /**
     * 触发器
     *
     * @return
     */
    public Trigger trigger(JobDetail jobDetail, IntervalJobInterface intervalJobs) {
        return TriggerBuilder
                .newTrigger()
                .withIdentity(intervalJobs.jobName(), intervalJobs.jobGroup())
                .forJob(jobDetail)
                .startNow()
                .withSchedule(intervalJobs.scheduleBuilder())
                .build();
    }


}

此处的类使用了@Component注解,由于我们这个类中无需要获取的属性,这里使用@Configuration同样可以,甚至getInitBeans()也可以用@Bean注解。此处使用@PostConstruct注解是为了保证在容器加载完后会执行该方法,以完成定时任务的获取和后续执行。

立即执行

一般来讲,由CronScheduleBuilder构建的定时任务并不会启动后就立即执行(Trigger中添加了startNow(),但仅对SimpleScheduleBuilder生效),因此可以通过如下代码使其生效

// crontab 表达式的任务不会立即执行,如需立即执行则取消如下条件判断代码的注释
if (intervalJobs.scheduleBuilder() instanceof CronScheduleBuilder) {
       scheduler.triggerJob(jobDetail.getKey());
}

可以看到由CronScheduleBuilder构建的任务在SpringBoot启动后立即执行。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是 SpringBoot 搭配 Quartz 实现动态定时任务的源码: 1. 首先,我们需要引入 QuartzSpringBoot 的依赖: ```xml <!-- Quartz相关依赖 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.3.2</version> </dependency> <!-- SpringBoot相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> ``` 2. 创建定时任务实体类,用于封装定时任务的信息,包括任务名称、任务组、任务类名、任务状态(是否启用)、任务表达式等: ```java @Entity @Table(name = "job_task") @Data public class JobTask implements Serializable { private static final long serialVersionUID = 1L; /** * ID */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 任务名称 */ @NotBlank(message = "任务名称不能为空") private String name; /** * 任务分组 */ @NotBlank(message = "任务分组不能为空") private String group; /** * 任务类名 */ @NotBlank(message = "任务类名不能为空") private String className; /** * 任务状态,0:禁用,1:启用 */ @NotNull(message = "任务状态不能为空") private Integer status; /** * 任务表达式 */ @NotBlank(message = "任务表达式不能为空") private String cronExpression; /** * 创建时间 */ private LocalDateTime createTime; /** * 最后一次修改时间 */ private LocalDateTime updateTime; } ``` 3. 创建定时任务的服务类,用于管理定时任务的增删改查等操作,同时也需要实现 `InitializingBean` 接口,在启动应用时加载已存在的定时任务: ```java @Service @AllArgsConstructor public class JobTaskService implements InitializingBean { private final Scheduler scheduler; private final JobTaskRepository jobTaskRepository; /** * 添加任务 * @param jobTask * @return * @throws Exception */ public boolean addJobTask(JobTask jobTask) throws Exception { if (jobTask == null || StringUtils.isBlank(jobTask.getCronExpression())) { return false; } if (StringUtils.isBlank(jobTask.getName()) || StringUtils.isBlank(jobTask.getClassName())) { throw new Exception("任务名称或任务类名不能为空"); } // 判断任务是否已存在 JobKey jobKey = JobKey.jobKey(jobTask.getName(), jobTask.getGroup()); if (scheduler.checkExists(jobKey)) { return false; } // 构建任务实例 JobDetail jobDetail = JobBuilder.newJob(getClass(jobTask.getClassName()).getClass()) .withIdentity(jobTask.getName(), jobTask.getGroup()) .build(); // 构建任务触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()); CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(jobTask.getName(), jobTask.getGroup()) .withSchedule(cronScheduleBuilder) .build(); // 注册任务和触发器 scheduler.scheduleJob(jobDetail, trigger); // 如果任务状态为启用,则立即启动任务 if (jobTask.getStatus() == 1) { scheduler.triggerJob(jobKey); } // 保存任务信息 jobTask.setCreateTime(LocalDateTime.now()); jobTask.setUpdateTime(LocalDateTime.now()); jobTaskRepository.save(jobTask); return true; } /** * 修改任务 * @param jobTask * @return * @throws Exception */ public boolean modifyJobTask(JobTask jobTask) throws Exception { if (jobTask == null || StringUtils.isBlank(jobTask.getCronExpression())) { return false; } if (StringUtils.isBlank(jobTask.getName()) || StringUtils.isBlank(jobTask.getClassName())) { throw new Exception("任务名称或任务类名不能为空"); } // 判断任务是否存在 JobKey jobKey = JobKey.jobKey(jobTask.getName(), jobTask.getGroup()); if (!scheduler.checkExists(jobKey)) { return false; } // 修改任务触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()); CronTrigger newTrigger = TriggerBuilder.newTrigger() .withIdentity(jobTask.getName(), jobTask.getGroup()) .withSchedule(cronScheduleBuilder) .build(); scheduler.rescheduleJob(TriggerKey.triggerKey(jobTask.getName(), jobTask.getGroup()), newTrigger); // 修改任务信息 JobTask oldJobTask = jobTaskRepository.findByNameAndGroup(jobTask.getName(), jobTask.getGroup()); oldJobTask.setClassName(jobTask.getClassName()); oldJobTask.setStatus(jobTask.getStatus()); oldJobTask.setCronExpression(jobTask.getCronExpression()); oldJobTask.setUpdateTime(LocalDateTime.now()); jobTaskRepository.save(oldJobTask); return true; } /** * 删除任务 * @param name * @param group * @return * @throws Exception */ public boolean deleteJobTask(String name, String group) throws Exception { JobKey jobKey = JobKey.jobKey(name, group); if (!scheduler.checkExists(jobKey)) { return false; } scheduler.deleteJob(jobKey); jobTaskRepository.deleteByNameAndGroup(name, group); return true; } /** * 获取所有任务 * @return */ public List<JobTask> getAllJobTask() { return jobTaskRepository.findAll(); } /** * 根据任务名称和分组获取任务信息 * @param name * @param group * @return */ public JobTask getJobTaskByNameAndGroup(String name, String group) { return jobTaskRepository.findByNameAndGroup(name, group); } /** * 获取任务类实例 * @param className * @return * @throws Exception */ private Object getClass(String className) throws Exception { Class<?> clazz = Class.forName(className); return clazz.newInstance(); } /** * 实现 InitializingBean 接口,在启动应用时加载已存在的定时任务 * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { List<JobTask> jobTaskList = jobTaskRepository.findAll(); for (JobTask jobTask : jobTaskList) { if (jobTask.getStatus() == 1) { addJobTask(jobTask); } } } } ``` 4. 创建定时任务的控制器类,用于处理新增、修改、删除等请求: ```java @RestController @AllArgsConstructor @RequestMapping("/job") public class JobTaskController { private final JobTaskService jobTaskService; /** * 添加任务 * @param jobTask * @return * @throws Exception */ @PostMapping public ResponseEntity addJobTask(@RequestBody JobTask jobTask) throws Exception { boolean result = jobTaskService.addJobTask(jobTask); return result ? ResponseEntity.ok("任务添加成功") : ResponseEntity.badRequest().body("任务添加失败"); } /** * 修改任务 * @param jobTask * @return * @throws Exception */ @PutMapping public ResponseEntity modifyJobTask(@RequestBody JobTask jobTask) throws Exception { boolean result = jobTaskService.modifyJobTask(jobTask); return result ? ResponseEntity.ok("任务修改成功") : ResponseEntity.badRequest().body("任务修改失败"); } /** * 删除任务 * @param name * @param group * @return * @throws Exception */ @DeleteMapping("/{name}/{group}") public ResponseEntity deleteJobTask(@PathVariable String name, @PathVariable String group) throws Exception { boolean result = jobTaskService.deleteJobTask(name, group); return result ? ResponseEntity.ok("任务删除成功") : ResponseEntity.badRequest().body("任务删除失败"); } /** * 获取所有任务 * @return */ @GetMapping public ResponseEntity getAllJobTask() { List<JobTask> jobTaskList = jobTaskService.getAllJobTask(); return ResponseEntity.ok(jobTaskList); } /** * 根据任务名称和分组获取任务信息 * @param name * @param group * @return */ @GetMapping("/{name}/{group}") public ResponseEntity getJobTaskByNameAndGroup(@PathVariable String name, @PathVariable String group) { JobTask jobTask = jobTaskService.getJobTaskByNameAndGroup(name, group); return ResponseEntity.ok(jobTask); } } ``` 5. 创建定时任务的启动类,用于启动 SpringBoot 应用: ```java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 注册定时任务调度器 * @return * @throws SchedulerException */ @Bean public SchedulerFactoryBean schedulerFactoryBean() throws SchedulerException { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); Properties properties = new Properties(); properties.put("org.quartz.scheduler.instanceName", "ChitGPTScheduler"); properties.put("org.quartz.threadPool.threadCount", "10"); schedulerFactoryBean.setQuartzProperties(properties); schedulerFactoryBean.setStartupDelay(5); return schedulerFactoryBean; } /** * 注册定时任务实例 * @return */ @Bean public Scheduler scheduler() { return schedulerFactoryBean().getScheduler(); } } ``` 以上就是 SpringBoot 搭配 Quartz 实现动态定时任务的源码,希望能对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值