深入浅出,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 {@Autowiredprivate 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 {@Autowiredprivate RedisOperator redisOperator;/*** Redis连接状态标识*/public static boolean redisConnected;@Overrideprotected 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 {@Autowiredprivate RedisOperator redisOperator;/*** 任务名称*/public String name = "redis-check";/*** 分组名称*/public String group = "redis";/*** 任务执行周期,单位s*/public Integer intervalTime = 60;/*** redis连接状态*/public static boolean redisConnected;@Overridepublic String jobName() {return name;}@Overridepublic String jobGroup() {return group;}/*** 不同定时器** @return*/@Overridepublic ScheduleBuilder scheduleBuilder() {return SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(intervalTime)// 永不过期.repeatForever();}/*** 定时任务逻辑**/@Overridepublic 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 {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate Scheduler scheduler;/*** Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)* 获取所有实现了任务标记接口类,*/@PostConstructpublic 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.hqwc.cn/news/129069.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ExoPlayer架构详解与源码分析(4)——整体架构

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…

【C/C++】结构体内存分配问题

规则1&#xff1a;以多少个字节为单位开辟内存 就是说&#xff0c;该结构体最终所占字节大小&#xff0c;是这个单位的整数倍 给结构体变量分配内存的时候&#xff0c;会去结构体变量中找基本类型的成员 哪个基本类型的成员占字节数多&#xff0c;就以它大大小为单位开辟内存 …

竞赛选题 深度学习 python opencv 火焰检测识别 火灾检测

文章目录 0 前言1 基于YOLO的火焰检测与识别2 课题背景3 卷积神经网络3.1 卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 YOLOV54.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 数据集准备5.1 数…

快手商品数据整合API|获取快手商品详情数据价格销量主图宝贝链接

接口名称&#xff1a;ks.item_get 接口路径&#xff1a;https://api-seaver.cn/ks/item_get 功能介绍&#xff1a; 通过调用此API可获取商品详情数据&#xff0c;包括商品ID、宝贝标题、商品简介、价格、原价、掌柜昵称、库存、宝贝链接、宝贝图片、品牌名称、商品详情、商品…

Visual Studio 2022 修改字符集的方法

在射频识别技术课程实验过程中发现的报错问题&#xff0c;搞了半天才找到原因&#xff0c;是字符集设置有问题。下图为报错&#xff1a; 根本原因是默认的字符编码集是Unicode。 改成使用多字节字符集就好了。以下为修改方法。

vue3 antv 静态登录页面

效果图 <template> <!-- 内容区域 --><div class"main"><div class"from"><!-- 表单 model是antv里边的绑定表单数据 --><a-form :model"formState" ref"formRef"><!-- 切换 --><a-tabs…

手写模拟SpringBoot核心流程

通过手写模拟实现一个Spring Boot&#xff0c;让大家能以非常简单的方式就能知道Spring Boot大概是如何工作的。 依赖 建一个工程&#xff0c;两个Module: 1.springboot模块&#xff0c;表示springboot框架的源码实现 2.user包&#xff0c;表示用户业务系统&#xff0c;用来写…

【刷题】只出现一次的数字(三种解法)

【刷题】只出现一次的数字 文章目录 【刷题】只出现一次的数字解法异或运算解法一 : 异或运算解法二:集合类Set集合Map集合 链接: https://www.nowcoder.com/share/jump/2008263481696810321082 https://leetcode.cn/problems/single-number/description/ 题目描述 给定一个整…

基于Java的民宿管理系统设计与实现(源码+lw+部署文档+讲解等)(民宿预约、民宿预订、民宿管理、酒店预约通用)

文章目录 前言具体实现截图论文参考详细视频演示代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技…

运行软件找不到mfc140u.dll怎么解决,mfc140u.dll是什么文件

"找不到 mfc140u.dll"是一条错误信息&#xff0c;表示您的计算机上缺少一个名为 mfc140u.dll 的动态链接库&#xff08;DLL&#xff09;文件。这个文件通常与 Microsoft Visual C Redistributable 相关。Mfc140u.dll 是 Microsoft 基础类库&#xff08;MFC&#xff0…

Linux: 基础IO

学习目标 1.C接口与系统调用接口的差别 2.文件描述符, 重定向, 一切皆文件, 缓冲区 3.fd与FILE, 系统调用和库函数的关系 4.系统中的inode 5.软硬链接 6.动静态库 预备知识 1.文件 内容 属性 2.文件的所有操作: a. 对内容的操作 b.对属性的操作 3.文件在磁盘(硬件)上, 我…

Blender 导出 fbx 到虚幻引擎中丢失材质!!!(使用Blender导出内嵌材质的fbx即可解决)

目录 0 引言1 Blender导出内嵌纹理的fbx模型 0 引言 我在Blender处理了一些fbx模型后再次导出到UE中就经常出现&#xff0c;材质空白的情况&#xff08;如下图所示&#xff09;&#xff0c;今天终于找到问题原因&#xff0c;记录下来&#xff0c;让大家避免踩坑。 其实原因很简…