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

news2025/1/12 10:57:05

前言

在上一篇深入浅出,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启动后立即执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1067668.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

redis分布式秒杀锁

-- 获取锁标识&#xff0c;是否与当前线程一致&#xff1f; if(redis.call(get, KEYS[1]) ARGV[1]) then-- 一致&#xff0c;删除return redis.call(del, KEYS[1]) end -- 不一致&#xff0c;直接返回 return 0package com.platform.lock;public interface ILock {/*** 获取锁…

2.5 数字传输系统

笔记&#xff1a; 针对这一节的内容&#xff0c;我为您提供一个笔记的整理方法。将内容按重要性、逻辑关系进行组织&#xff0c;再进行简化。 ## 2.5 数字传输系统 ### 背景介绍&#xff1a; 1. **早期电话网**&#xff1a;市话局到用户采用双绞线电缆&#xff0c;长途干线采…

Ubuntu 20.04使用源码安装nginx 1.14.0

nginx安装及使用&#xff08;详细版&#xff09;是一篇参考博文。 http://nginx.org/download/可以选择下载源码的版本。 sudo wget http://nginx.org/download/nginx-1.14.0.tar.gz下载源代码。 sudo tar xzf nginx-1.14.0.tar.gz进行解压。 cd nginx-1.14.0进入到源代码…

ping使用

使用shell ping一个网段 #!/bin/shfor ib in $(seq 1 254); doip"192.168.1.$ib"(if ping -c3 "$ip" >> 1.txt; thenecho "$ip is alive"fi) &done wait在每次循环的最后&#xff0c;使用 & 将子 shell 放入后台执行&#xff0c…

怒刷LeetCode的第25天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;闭合为环 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;动态规划 方法二&#xff1a;组合数学 方法三&#xff1a;递归 方法四&#xff1a;数学公式 第三题 题目来源 题目内容 解决方法 …

ctfshow-web3(伪协议php://input的利用)

打开题目链接&#xff0c;存在文件包含函数 include 存在文件包含时直接使用PHP伪协议 先介绍一下一个常用的php伪协议&#xff1a; php://input&#xff08;用于执行PHP代码&#xff09; 这是一个只读信息流&#xff0c;当请求方式是post的&#xff0c;并且enctype不等于…

Flink---10、处理函数(基本处理函数、按键分区处理函数、窗口处理函数、应用案例TopN、侧输出流)

星光下的赶路人star的个人主页 我的敌手就是我自己&#xff0c;我要他美好到能使我满意的程度 文章目录 1、处理函数1.1 基本处理函数&#xff08;ProcessFunction&#xff09;1.1.1 处理函数的功能和使用1.1.2 ProcessFunction解析1.1.3 处理函数的分类 1.2 按键分区处理函数&…

Charles:移动端抓包 / windows客户端 iOS手机

一、背景描述 1.1、本文需求&#xff1a;移动端进行抓包调试 1.2、理解Charles可以做什么 Charles是一款跨平台的网络代理软件&#xff0c;可以用于捕获和分析网络流量&#xff0c;对HTTP、HTTPS、HTTP/2等协议进行调试和监控。使用Charles可以帮助开发人员进行Web开发、调试…

【MySQL】Linux 中 MySQL 环境的安装与卸载

文章目录 Linux 中 MySQL 环境的卸载Linux 中 MySQL 环境的安装 Linux 中 MySQL 环境的卸载 在安装 MySQL 前&#xff0c;我们需要先将系统中以前的环境给卸载掉。 1、查看以前系统中安装的 MySQL rpm -qa | grep mysql2、卸载这些 MySQL rpm -qa | grep mysql | args yum …

【Unity】RenderFeature笔记

【Unity】RenderFeature笔记 RenderFeature是在urp中添加的额外渲染pass&#xff0c;并可以将这个pass插入到渲染列队中的任意位置。内置渲染管线中Graphics 的功能需要在RenderFeature里实现,常见的如DrawMesh和Blit ​ 可以实现的效果包括但不限于 后处理&#xff0c;可以编写…

ruoyi-cloud新增模块示例(亲自操作,可行)

1.在ruoyi-modules模块下新建模块&#xff08;ruoyi-search&#xff09; 2.重命名模块的名称 3.在ruoyi-modules添加新模块依赖 4.复制ruoyi-system模块的pom文件&#xff0c;并进行修改 <?xml version"1.0" encoding"UTF-8"?> <project xmlns…

JetPack系列:001-JetPack概要介绍

文章目录 1. 概念介绍2. 主要内容2.1 框架库2.2 UI界面库 3. 核心思想4. 内容总结 本章回是一起Talk AndroidJetpack吧专栏的第一章回&#xff0c;本章回中主要介绍JetPack的基本概念和编程思想&#xff0c;同时也会介绍它的基础知识。闲话休提&#xff0c;请我们一起Talk Andr…

Mysql——创建数据库,对表的创建及字段定义、数据录入、字段增加及删除、重命名表。

一.创建数据库 create database db_classics default charsetutf8mb4;//创建数据库 use db_classics;//使用该数据库二.对表的创建及字段定义 create table if not exists t_hero ( id int primary key auto_increment, Name varchar(100) not null unique, Nickname varchar(1…

TDengine OSS 与 qStudio 实现无缝协同,革新数据分析和管理方式

在数字化转型如火如荼的当下&#xff0c;海量爆发的时序数据处理成为转型成功的关键因素之一。为了帮助社区用户更好地进行数据分析和管理&#xff0c;丰富可视化解决方案的多样性&#xff0c;我们将开源的时序数据库&#xff08;Time Series Database&#xff09; TDengine OS…

【Zookeeper专题】Zookeeper经典应用场景实战(一)

目录 前置知识课程内容一、Zookeeper Java客户端实战1.1 Zookeeper 原生Java客户端使用1.2 Curator开源客户端使用快速开始使用示例 二、Zookeeper在分布式命名服务中的实战2.1 分布式API目录2.2 分布式节点的命名2.3 分布式的ID生成器 三、zookeeper实现分布式队列3.1 设计思路…

SpringBoot集成MyBatis-Plus实现增删改查

背景 因为学习工具的时候经常需要用到jar包&#xff0c;需要增删查改接口&#xff0c;所以参考文章实现了基于mybatis-plus的增删查改接口。 参考文章&#xff1a;第二十二节:SpringBoot集成MyBatis-Plus实现增删改查 原文中的git地址不存在&#xff0c;本文内容是原文代码修…

基于Java的源代码共享平台设计与实现(源码+lw+ppt+部署文档+视频讲解等)

文章目录 前言系统功能具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域…

C#学习系列相关之多线程(二)----Thread类介绍

一、线程初始化 1.无参数 static void Main(string[] args) {//第一种写法Thread thread new Thread(test);thread.Start();//第二种写法 delegateThread thread1 new Thread(new ThreadStart(test));thread1.Start();//第三种写法 lambdaThread thread2 new Thread(() >…

R语言提交后台任务Rstudio\nohup

R语言后台任务用法 在进行大规模数据分析时&#xff0c;R语言提供了后台计算的功能&#xff0c;能将计算任务提交到后台执行&#xff0c;不影响当前窗口的活动&#xff0c;而且不会受到网络波动导致任务中断&#xff0c;提交后就不用盯着一直看&#xff0c;后台运行就可以下班。…

3.1.OpenCV技能树--二值图像处理--阈值

文章目录 1.文章内容来源2.阈值分割2.1.简单阈值分割2.1.1.简单阈值分割原理介绍2.1.2.简单阈值分割核心代码2.1.3.简单阈值分割效果展示 2.2.自适应阈值分割2.2.1.自适应阈值分割原理介绍2.2.2.自适应阈值分割核心代码2.2.3.自适应阈值分割效果展示 2.3.Otsu’s二值化/大津阈值…