SpringCache在redis不可用时不会执行原方法的一些解决方法

项目里用了@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类的方式。

java - spring aop that intercepts org.springframework.cache.interceptor.CacheInterceptor#invoke - Stack Overflow

全局捕获异常及定时任务的代码逻辑不变

@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;
        }
    }
}

这两部分代码放到项目里,就能实现与方法一完全一致的效果了,而且这个最主要的是不会入侵业务代码!需要就复制粘贴进去,不需要删除就行。我个人很满意!

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值