做網(wǎng)站找哪個(gè)軟件網(wǎng)店運(yùn)營具體做什么
前言
在上一篇深入淺出,SpringBoot整合Quartz實(shí)現(xiàn)定時(shí)任務(wù)與Redis健康檢測(一)_往事如煙隔多年的博客-CSDN博客
文章中對SpringBoot整合Quartz做了初步的介紹以及提供了一個(gè)基本的使用例子,因?yàn)閷?shí)際各自的需求任務(wù)不盡相同因此并未對定時(shí)任務(wù)的代碼做相關(guān)填充。本文將對Redis的健康檢測進(jìn)行進(jìn)一步的實(shí)現(xiàn),并且將嘗試逐步縮減相關(guān)代碼,一步步優(yōu)化定時(shí)任務(wù)的創(chuàng)建流程。
Redis定時(shí)檢測
由于項(xiàng)目本身是一個(gè)學(xué)習(xí)類型項(xiàng)目,因此實(shí)際的使用數(shù)據(jù)量并不會(huì)特別大。當(dāng)然引入Redis是考慮到在對熱點(diǎn)數(shù)據(jù)重復(fù)訪問時(shí)提升響應(yīng)速度,關(guān)于緩存的實(shí)現(xiàn)方式多種多樣,此處就以Redis為例。
問題復(fù)現(xiàn)
在引入Redis之后,某些接口在使用時(shí)需要從Redis中存取數(shù)據(jù),就需要保證Redis的存活狀態(tài),否則會(huì)拋出異常。而如果在項(xiàng)目使用之初并沒有配置Redis也將直接導(dǎo)致項(xiàng)目無法啟動(dòng)。此外在長時(shí)間不使用Redis時(shí)其連接池資源被釋也會(huì)導(dǎo)致第一次訪問時(shí)數(shù)據(jù)非常緩慢,以上的情景在使用時(shí)是非常糟糕的。
解決方案
在一些實(shí)際的開發(fā)場景中,遇到如上情況時(shí)可能會(huì)使用Hystrix來進(jìn)行熔斷,或者采用限流等措施。然而對于我們來講,能夠清楚的預(yù)見訪問流量的有限性,即使只使用單機(jī)的數(shù)據(jù)庫也能支撐服務(wù),那么能否在Redis不可用時(shí)切換到MySQL查詢呢?答案是可以的。
這里使用Redis中的ping命令來實(shí)現(xiàn)對Redis存活狀態(tài)的檢測,由于本項(xiàng)目中對于字符串類操作較多,這里工具使用了StringRedisTemplate進(jìn)行封裝。RedisOperator即為其它類中操作Redis的工具類,這里將其交由Spring容器管理。
@Component
public class RedisOperator {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 07.19 新增ping,用于redis健康檢測,連接失敗的異常將不會(huì)返回給前端,也不會(huì)進(jìn)入全局異常處理* @return*/public boolean ping(){boolean result;try{result = "PONG".equals(redisTemplate.getConnectionFactory().getConnection().ping());}catch (RedisConnectionFailureException e){log.error("捕獲Redis連接異常,請檢查服務(wù)運(yùn)行狀態(tài)!");result = false;}return result;}// 其它命令...}
上述代碼通過使用ping命令的返回值判斷Redis當(dāng)前的存活狀態(tài),那么實(shí)際的使用場景如何呢?以下是一個(gè)簡單例子:
可以看到如果不進(jìn)行檢測的話,此時(shí)Redis處于未連接狀態(tài)時(shí)將造成阻塞,該方法會(huì)一直等待判斷key是否存在解決狀態(tài)的返回,最終達(dá)到超時(shí)時(shí)間后將拋出異常,體驗(yàn)非常糟糕。?
if (redisOperator.keyIsExist(shopCategoryCacheKey.toString())) {// 獲取數(shù)據(jù)返回shopCategoryVOList = cacheOperator.readCache(shopCategoryCacheKey.toString(),new ArrayList<ShopCategoryVO>());return new PageVO(shopCategoryVOList, shopCategoryVOList.size());
}
那么如何檢測呢?如上述代碼不在少數(shù),也就是說每次執(zhí)行時(shí)都要發(fā)送ping命令執(zhí)行驗(yàn)證Redis存活狀態(tài),而Redis只有在連接狀態(tài)下才會(huì)及時(shí)返回,除此之外均需等待到超時(shí)結(jié)束返回異常。這里就可以用到前一篇文章的定時(shí)任務(wù)了,我們可以通過將ping命令執(zhí)行交給定時(shí)任務(wù)進(jìn)行檢測,然后維持一個(gè)boolean類型的標(biāo)識(shí),每次判別標(biāo)識(shí)即可。當(dāng)Redis處于未連接狀態(tài)時(shí)自然不會(huì)執(zhí)行上述代碼,進(jìn)而執(zhí)行數(shù)據(jù)庫查詢操作。
定時(shí)任務(wù)
由于上一篇文章中已經(jīng)編寫了配置類
@Configuration
@Slf4j
public class RedisCheckConfig {// 指定生成的Bean實(shí)例對象名稱@Bean("redisCheck")public JobDetail jobDetail() {return JobBuilder.newJob(RedisCheckJob.class)// 任務(wù)名和任務(wù)分組.withIdentity("RedisCheckJob", "group").withDescription("任務(wù)描述:內(nèi)存方式運(yùn)行").storeDurably().build();}@Bean("redisTrigger")public Trigger trigger() {return TriggerBuilder.newTrigger()// 觸發(fā)器名稱和分組.withIdentity("redisCheck", "group").forJob(jobDetail()).startNow()// 使用SimpleSchedule構(gòu)建定時(shí)任務(wù).withSchedule(SimpleScheduleBuilder.simpleSchedule()// 每隔10s執(zhí)行任務(wù).withIntervalInSeconds(10)// 永不過期.repeatForever()).build();}
}
因此這里只需要編寫實(shí)際的業(yè)務(wù)類即可,可以看到這里通過依賴注入獲取到RedisOperator類,它用于獲取ping命令執(zhí)行后的結(jié)果。
@Slf4j
public class RedisCheckJob extends QuartzJobBean {@Autowiredprivate RedisOperator redisOperator;/*** Redis連接狀態(tài)標(biāo)識(shí)*/public static boolean redisConnected;@Overrideprotected void executeInternal(JobExecutionContext context) throws JobExecutionException {log.info("開始檢測Redis連接狀態(tài)");redisConnected = redisOperator.ping();log.info("Redis當(dāng)前是否連接 "+ redisConnected);}
}
到這里就需要在進(jìn)行Redis操作的代碼前加入如下判斷即可,這樣就實(shí)現(xiàn)了Redis的一樣定期?;詈徒】禉z測,一旦Redis處于未連接狀態(tài)的將直接調(diào)用數(shù)據(jù)庫查詢,而定時(shí)器執(zhí)行檢測的間隔將由使用者自行設(shè)置。
// 判斷Redis存活狀態(tài)
if(RedisCheckJob.redisConnected){// Redis操作 if (redisOperator.keyIsExist(shopCategoryCacheKey.toString())) {// ....}
}
優(yōu)化
雖然前文中已經(jīng)對完成了文章開頭所需要的解決的問題,但每一次創(chuàng)建新的定時(shí)任務(wù)時(shí)均需要編寫JobDetail和Trigger代碼,似乎有些冗余,能否對其對進(jìn)一步優(yōu)化呢? 實(shí)際上對于普通的定時(shí)任務(wù)使用上述操作即可,此處為個(gè)人學(xué)習(xí)探究內(nèi)容,酌情觀看。
抽象類 VS 接口
考慮到需要構(gòu)建JobDetail和Trigger均需要使用name和group屬性,因此考慮使用一個(gè)類進(jìn)行管理,在SpringBoot中使用@Configuration和@Bean注解可以將定時(shí)任務(wù)配置類關(guān)聯(lián)到Scheduler中,而若手動(dòng)創(chuàng)建對象時(shí)則要考慮如何如何創(chuàng)建配置類?獲取配置類?如何調(diào)用的問題?
在上一篇文中提到可以通過繼承?QuartzJobBean?類并重寫其excuteInternal方法,或?qū)崿F(xiàn)?Job?接口的excute方法,從QuartzJobBean的源碼可知,其實(shí)現(xiàn)了Job接口,因此以上的創(chuàng)建方式任選其一即可。??
這里就涉及到一個(gè)選擇性的問題,最終編寫的實(shí)現(xiàn)類應(yīng)該具有任務(wù)名、分組名稱、觸發(fā)器這三個(gè)屬性,它們將用于后續(xù)任務(wù)的構(gòu)建。由于需要獲取這三個(gè)屬性,因此考慮使用抽象方法獲取,因?yàn)樽罱K的定時(shí)任務(wù)類為普通類,只負(fù)責(zé)信息的初始化,而目前來看若無需屬性控制且均為抽象方法時(shí),則可以將該類轉(zhuǎn)為接口,這里無論選擇抽象類還是接口,都需要確保最終實(shí)現(xiàn)了Job接口。
這里以接口為例構(gòu)建,scheduleBuilder()方法用于接收實(shí)現(xiàn)類的定時(shí)器,通過實(shí)現(xiàn)該方法將由子類中自行決定使用哪種定時(shí)器。
public interface IntervalJobInterface extends Job {/*** 任務(wù)名稱* @return*/String jobName();/*** 任務(wù)分組* @return*/String jobGroup();/*** 不同的任務(wù)定時(shí)器,由實(shí)現(xiàn)類構(gòu)建* @return*/ScheduleBuilder scheduleBuilder();
}
編寫定時(shí)任務(wù)類如下
@Slf4j
@Component
public class RedisCheckConfigForInterface implements IntervalJobInterface {@Autowiredprivate RedisOperator redisOperator;/*** 任務(wù)名稱*/public String name = "redis-check";/*** 分組名稱*/public String group = "redis";/*** 任務(wù)執(zhí)行周期,單位s*/public Integer intervalTime = 60;/*** redis連接狀態(tài)*/public static boolean redisConnected;@Overridepublic String jobName() {return name;}@Overridepublic String jobGroup() {return group;}/*** 不同定時(shí)器** @return*/@Overridepublic ScheduleBuilder scheduleBuilder() {return SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(intervalTime)// 永不過期.repeatForever();}/*** 定時(shí)任務(wù)邏輯**/@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {log.info("開始檢測Redis連接狀態(tài),IntervalInterface");redisConnected = redisOperator.ping();log.info("Redis當(dāng)前是否連接 "+ redisConnected);}
}
之后可以通過@Component或@Confiugration注解將其放入Spring的容器中,方便取用。?
@Component VS?@Confiugration
如下為獲取配置類信息的代碼,其作用為獲取?IntervalJobInterface 接口的所有實(shí)現(xiàn)類,然后通過實(shí)例獲取其任務(wù)名和分組名,上一步中提到實(shí)現(xiàn)類的編寫可以使用@Component或@Confiugration注解,那兩者有什么區(qū)別呢?一般來說@Configuration注解與@Bean注解作用于配置類上,但目前代碼中沒有使用@Bean注解生成Bean實(shí)例,那么是否可以在任務(wù)類上直接使用@Configuration注解呢?
這里需要特別注意,若需要在其它地方通過反射機(jī)制獲取如上任務(wù)類的屬性時(shí),@Configuration標(biāo)注的類將使用cglib生成代理類,反射獲取不能直接取得類信息獲得屬性,需要通過getClass().getSuperClass()的方式獲取。而@Component注解作用的類生成的原有的類實(shí)例,可以直接getClass()獲取類信息,再獲取屬性。
既然可以直接反射獲取其相關(guān)屬性,為什么還需要添加一個(gè)接口?由于反射獲取的屬性需要?jiǎng)?chuàng)建新對象重新組裝,JobDetail和Trigger都需要一個(gè)實(shí)例類信息,添加一個(gè)接口可以獲取信息的同時(shí)也能用作實(shí)例類的描述,詳見如下代碼:
@Component
@Slf4j
public class QuartzConfig {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate Scheduler scheduler;/*** Constructor(構(gòu)造方法) -> @Autowired(依賴注入) -> @PostConstruct(注釋的方法)* 獲取所有實(shí)現(xiàn)了任務(wù)標(biāo)記接口類,*/@PostConstructpublic void getInitBeans() {log.info("開始獲取定時(shí)任務(wù)");Map<String, IntervalJobInterface> intervalJobInterfaceMap = applicationContext.getBeansOfType(IntervalJobInterface.class);intervalJobInterfaceMap.forEach((className, jobInstance) -> {IntervalJobInterface intervalJobs = (IntervalJobInterface) jobInstance;log.info("任務(wù)名稱: " + intervalJobs.jobName());log.info("任務(wù)分組: " + intervalJobs.jobGroup());// 定時(shí)任務(wù)不存在,無法執(zhí)行if (intervalJobs.scheduleBuilder() == null) {log.error(className + " 任務(wù)無法運(yùn)行, 請指定任務(wù)的運(yùn)行周期時(shí)間后再試!");return;}JobDetail jobDetail = jobDetail(intervalJobs);Trigger trigger = trigger(jobDetail, intervalJobs);try {scheduler.scheduleJob(jobDetail, trigger);// crontab 表達(dá)式的任務(wù)不會(huì)立即執(zhí)行,如需立即執(zhí)行則取消如下條件判斷代碼的注釋//if (intervalJobs.scheduleBuilder() instanceof CronScheduleBuilder) {// scheduler.triggerJob(jobDetail.getKey());//}log.info("已添加 " + intervalJobs.jobName() + " 任務(wù) " + " jobKey" + jobDetail.getKey());} catch (SchedulerException e) {log.error("定時(shí)任務(wù)出現(xiàn)異常");e.printStackTrace();}});log.info("獲取定時(shí)任務(wù)結(jié)束");}/*** 任務(wù)詳情** @param intervalJobs* @return*/public JobDetail jobDetail(IntervalJobInterface intervalJobs) {return JobBuilder.newJob(intervalJobs.getClass()).withIdentity(intervalJobs.jobName(), intervalJobs.jobGroup()).withDescription("內(nèi)存運(yùn)行").storeDurably().build();}/*** 觸發(fā)器** @return*/public Trigger trigger(JobDetail jobDetail, IntervalJobInterface intervalJobs) {return TriggerBuilder.newTrigger().withIdentity(intervalJobs.jobName(), intervalJobs.jobGroup()).forJob(jobDetail).startNow().withSchedule(intervalJobs.scheduleBuilder()).build();}}
此處的類使用了@Component注解,由于我們這個(gè)類中無需要獲取的屬性,這里使用@Configuration同樣可以,甚至getInitBeans()也可以用@Bean注解。此處使用@PostConstruct注解是為了保證在容器加載完后會(huì)執(zhí)行該方法,以完成定時(shí)任務(wù)的獲取和后續(xù)執(zhí)行。
立即執(zhí)行
一般來講,由CronScheduleBuilder構(gòu)建的定時(shí)任務(wù)并不會(huì)啟動(dòng)后就立即執(zhí)行(Trigger中添加了startNow(),但僅對SimpleScheduleBuilder生效),因此可以通過如下代碼使其生效
// crontab 表達(dá)式的任務(wù)不會(huì)立即執(zhí)行,如需立即執(zhí)行則取消如下條件判斷代碼的注釋
if (intervalJobs.scheduleBuilder() instanceof CronScheduleBuilder) {scheduler.triggerJob(jobDetail.getKey());
}
可以看到由CronScheduleBuilder構(gòu)建的任務(wù)在SpringBoot啟動(dòng)后立即執(zhí)行。