项目里用了@Cacheable注解后,如果出现网络连接不上redis,不仅需要等待一个超时时间,它还不会执行原方法直接抛出异常就结束了,这个使用体验贼离谱。琢磨了一些方法来解决这个离谱的场景,让项目能在redis不可用时停用@Cacheable注解的逻辑,redis可用时又能恢复使用。
目录
方法一:利用SpringCache注解提供的condition属性
方法二:重写CacheInterceptor类(个人更推荐的)
方法一:利用SpringCache注解提供的condition属性,判断某个静态变量为true时才会使@Cacheable注解生效。
定义一个静态变量做让注解判断是否生效的标志。
public class DefineParams {
/**
* 是否使springcache相关注解生效标志
*/
public volatile static boolean useCacheFalg=true;
}
设置全局捕获异常类来捕获redis连接异常,捕获到异常后将标志设为false。
@ControllerAdvice
public class GlobalExecptionHandle {
@ExceptionHandler(RedisConnectionFailureException.class)
public void handleRedisConnectionFailureException(RedisConnectionFailureException e) {
System.out.println("拦截到了redis连接异常");
DefineParams.useCacheFlag = false;
throw e;
}
}
设置一个定时任务,定期尝试连接redis来检查redis服务是否正常来设置useCacheFalg标志(全局捕获异常不会捕获定时任务抛出的异常,所以异常部分还是自己手动捕获处理)
@EnableScheduling
public class ScheduleWork {
@Autowired
private RedisTemplate redisTemplate;
@Async
@Scheduled(cron = "0/30 * * * * ?")
public void heardbeatRedis() {
try {
redisTemplate.getConnectionFactory().getConnection().ping();
DefineParams.useCacheFlag = true;
} catch (Exception e) {
System.out.printf("例行检查redis异常");
DefineParams.useCacheFlag = false;
e.printStackTrace();
}
}
}
业务代码方面,加上这部分condition条件。chatgpt跟我说这块代码为什么这么写的
T(com.example.package.StaticClass).ifCacheFlag
是一个SpEL表达式,用于访问StaticClass
类(需要提供完整的类名,包括包路径)中的ifCacheFlag
静态字段。T()
是SpEL中用于指定类类型的操作符。
@Cacheable(value = "normalCache", key = "#record.pid", condition = "#record.pid != null and T(com.zhou.pa.common.DefineParams).useCacheFalg")
@RequestMapping("/normalRedis")
public Record normalRedis(Record record) {
System.out.println("没命中缓存傻了吧");
return record;
}
这4部分搞完,就能做到让项目能在redis不可用时停用@Cacheable注解的逻辑,redis可用时又能恢复使用了。
方法二:重写CacheInterceptor类的方式做到与方法一类似的效果
方法一做出来之后我始终觉得不舒服,这已经入侵业务代码了,这个condition条件如果能通过配置类全局配置的方式就好了,但是SpringCache不提供这块的功能。就继续寻找一些减少影响业务代码的方案了。
后面在stackoverflow上,找到了个重写CacheInterceptor类的方式。
全局捕获异常及定时任务的代码逻辑不变
@ControllerAdvice
@EnableScheduling
public class ScheduleWork {
@Autowired
private RedisTemplate redisTemplate;
@Async
@Scheduled(cron = "0/30 * * * * ?")
public void heardbeatRedis() {
try {
redisTemplate.getConnectionFactory().getConnection().ping();
CustomRedisCacheConfig.useCacheFlag = true;
} catch (Exception e) {
System.out.printf("例行检查redis异常");
CustomRedisCacheConfig.useCacheFlag = false;
e.printStackTrace();
}
}
@ExceptionHandler(RedisConnectionFailureException.class)
public void handleRedisConnectionFailureException(RedisConnectionFailureException e) {
System.out.println("拦截到了redis连接异常");
CustomRedisCacheConfig.useCacheFlag = false;
throw e;
}
}
重写CacheInterceptor类,根据useCacheFlag 标志判断是否提前返回原方法的调用
@Configuration
public class CustomRedisCacheConfig{
/**
* 是否使用springcache相关注解标志
*/
public volatile static boolean useCacheFlag = true;
@Bean
public CacheInterceptor cacheInterceptor() {
CacheInterceptor interceptor = new TestMyCacheInterceptor();
interceptor.setCacheOperationSource(new AnnotationCacheOperationSource());
//使用自定义异常处理得考虑是否需要担心以后影响功能实现
//interceptor.setErrorHandler(new CustomCacheErroeHandler());
return interceptor;
}
public class TestMyCacheInterceptor extends CacheInterceptor {
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
} catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
//多加一行代码,如果useCacheFalg为false直接调用原方法
if (!useCacheFlag) return aopAllianceInvoker.invoke();
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
} catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
} catch (RedisConnectionFailureException e) {
//捕获一下springcache抛出的RedisConnectionFailureException异常,让当前出问题的方法能正常调用
useCacheFlag = false;
return aopAllianceInvoker.invoke();
}
}
}
/**
* 在这些实现方法里只要不继续往外抛异常,就可以做到出现异常的那次请求虽然会等待两个超时时间,但是至少会继续执行原方法
* 但是这个在我看来有点危险,会导致全局异常拦截器捕获不到这块异常,也不想看到以后在实现全局记录异常时出现记录不到特定问题吧
*/
public class CustomCacheErroeHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
System.out.println("到达handleCacheGetError");
useCacheFlag = false;
//throw exception;
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
System.out.println("??handleCachePutError");
useCacheFlag = false;
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
System.out.println("??handleCacheEvictError");
useCacheFlag = false;
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
System.out.println("??handleCacheClearError");
useCacheFlag = false;
}
}
}
这两部分代码放到项目里,就能实现与方法一完全一致的效果了,而且这个最主要的是不会入侵业务代码!需要就复制粘贴进去,不需要删除就行。我个人很满意!