如果 redis 连接失败,如何在运行时禁用 Redis 缓存

Posted

技术标签:

【中文标题】如果 redis 连接失败,如何在运行时禁用 Redis 缓存【英文标题】:How to disable Redis Caching at run time if redis connection failed 【发布时间】:2015-05-14 06:08:36 【问题描述】:

我们有 rest api 应用程序。我们使用 redis 进行 API 响应缓存和内部方法缓存。如果 redis 连接,那么它会使我们的 API 关闭。如果 redis 连接失败或出现任何异常,我们希望绕过 redis 缓存,而不是关闭我们的 API。 有一个接口 CacheErrorHandler 但它处理 redis 获取设置操作失败而不是 redis 连接问题。我们使用的是 Spring 4.1.2。

【问题讨论】:

在 Spring Boot 的情况下 - 尝试关注这个answer 【参考方案1】:

让我们稍微简化一下。您的应用程序使用缓存(使用 Redis 实现)。如果 Redis 连接过时/关闭或其他情况,那么您希望应用程序绕过缓存并(可能)直接进入底层数据存储(例如 RDBMS)。应用程序服务逻辑可能类似于...

@Service
class CustomerService ... 

    @Autowired
    private CustomerRepository customerRepo;

    protected CustomerRepository getCustomerRepo() 
        Assert.notNull(customerRepo, "The CustomerRepository was not initialized!");
        return customerRepo;
    

    @Cacheable(value = "Customers")
    public Customer getCustomer(Long customerId) 
        return getCustomerRepo().load(customerId);
    
    ...

在 Spring 核心的缓存抽象中确定缓存“未命中”的所有重要因素是返回的值为 null。因此,Spring Caching Infrastructure 将继续调用实际的 Service 方法(即 getCustomer)。请记住,在 getCustomerRepo().load(customerId) 调用的返回上,您还需要处理 Spring 的 Caching Infrastructure 现在尝试缓存该值的情况。

本着保持简单的精神,我们将不用 AOP,但您也应该能够使用 AOP 来实现这一点(您的选择)。

您(应该)需要的只是一个扩展 SDR CacheManager implementation 的“自定义”RedisCacheManager,类似于...

package example;

import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
...

class MyCustomRedisCacheManager extends RedisCacheManager 

    public MyCustomerRedisCacheManager(RedisTemplate redisTemplate) 
        super(redisTemplate);
    

    @Override
    public Cache getCache(String name) 
        return new RedisCacheWrapper(super.getCache(name));
    


    protected static class RedisCacheWrapper implements Cache 

        private final Cache delegate;

        public RedisCacheWrapper(Cache redisCache) 
            Assert.notNull(redisCache, "'delegate' must not be null");
            this.delegate = redisCache;
        

        @Override
        public Cache.ValueWrapper get(Object key) 
            try 
              delegate.get(key);
            
            catch (Exception e) 
                return handleErrors(e);
            
        

        @Override
        public void put(Object key, Object value) 
            try 
                delegate.put(key, value);
            
            catch (Exception e) 
                handleErrors(e);
            
        

        // implement clear(), evict(key), get(key, type), getName(), getNativeCache(), putIfAbsent(key, value) accordingly (delegating to the delegate).

        protected <T> T handleErrors(Exception e) throws Exception 
            if (e instanceof <some RedisConnection Exception type>) 
                // log the connection problem
                return null;
            
            else if (<something different>)  // act appropriately 
            ...
            else 
                throw e;
            
        
    

因此,如果 Redis 不可用,也许您能做的最好的事情就是记录问题并继续让服务调用发生。显然,这会影响性能,但至少会提高人们对存在问题的认识。显然,这可以与更强大的通知系统相关联,但这是可能性的一个粗略示例。重要的是,您的服务仍然可用,而应用程序服务所依赖的其他服务(例如 Redis)可能已经失败。

在这个实现中(与我之前的解释相比)我选择委托给底层的实际 RedisCache 实现来让异常发生,然后充分了解 Redis 存在问题,以便您可以适当地处理异常.但是,如果您在检查时确定 Exception 与连接问题有关,您可以返回“null”,让 Spring Caching Infrastructure 像缓存“未命中”一样继续进行(即错误的 Redis 连接 == 缓存未命中,在这种情况下)。

我知道这样的事情应该可以帮助您解决问题,因为我为 GemFire 和 Pivotal 的一位客户构建了一个类似的“自定义”CacheManager 实现原型。在那个特定的 UC 中,缓存“未命中”必须由应用程序域对象的“过期版本”触发,其中生产环境中混合了新旧应用程序客户端通过 Spring 的缓存抽象连接到 GemFire。例如,应用程序域对象字段会在较新版本的应用程序中发生变化。

无论如何,希望这对您有所帮助或给您更多想法。

干杯!

【讨论】:

这在 Spring Data Redis 2.x 中不起作用,因为 RedisCacheManager 的构造函数不接受 RedisTemplate。 @JohnBlum 有什么建议吗? 有同样的问题。我尝试对 RedisCacheManager 实现类似的扩展,但由于其中的更改以及字段都是私有的事实,现在实际上不可能。 @JohnBlum 你知道现在有没有办法做到这一点?【参考方案2】:

所以,我今天正在挖掘核心 Spring 框架缓存抽象源来解决另一个问题,如果 CacheErrorHandler 正确实现,那么有问题的 Redis 连接可能仍然会导致所需的行为,例如缓存“未命中”(由返回空值触发)。

请参阅AbstractCacheInvoker 来源了解更多详情。

cache.get(key) 应该会由于 Redis 连接错误而导致异常,因此将调用异常处理程序...

catch (RuntimeException e) 
    getErrorHandler().handleCacheGetError(e, cache, key);
    return null; // If the exception is handled, return a cache miss

如果 CacheErrorHandler 正确处理了缓存“get”错误(并且不重新抛出/异常),则将返回一个空值,指示缓存“未命中”。

【讨论】:

这适用于 RuntimeException 但不处理连接异常。【参考方案3】:

谢谢@John Blum。我在Spring Boot的解决方案如下。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;

import java.util.concurrent.Callable;

class CustomRedisCacheManager extends RedisCacheManager 
    private static Logger logger = LoggerFactory.getLogger(CustomRedisCacheManager.class);

    public CustomRedisCacheManager(RedisOperations redisOperations) 
        super(redisOperations);
    

    @Override
    public Cache getCache(String name) 
        return new RedisCacheWrapper(super.getCache(name));
    


    protected static class RedisCacheWrapper implements Cache 

        private final Cache delegate;

        public RedisCacheWrapper(Cache redisCache) 
            Assert.notNull(redisCache, "delegate cache must not be null");
            this.delegate = redisCache;
        

        @Override
        public String getName() 
            try 
                return delegate.getName();
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public Object getNativeCache() 
            try 
                return delegate.getNativeCache();
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public Cache.ValueWrapper get(Object key) 
            try 
                return delegate.get(key);
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public <T> T get(Object o, Class<T> aClass) 
            try 
                return delegate.get(o, aClass);
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public <T> T get(Object o, Callable<T> callable) 
            try 
                return delegate.get(o, callable);
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public void put(Object key, Object value) 
            try 
                delegate.put(key, value);
             catch (Exception e) 
                handleException(e);
            
        

        @Override
        public ValueWrapper putIfAbsent(Object o, Object o1) 
            try 
                return delegate.putIfAbsent(o, o1);
             catch (Exception e) 
                return handleException(e);
            
        

        @Override
        public void evict(Object o) 
            try 
                delegate.evict(o);
             catch (Exception e) 
                handleException(e);
            
        

        @Override
        public void clear() 
            try 
                delegate.clear();
             catch (Exception e) 
                handleException(e);
            
        

        private <T> T handleException(Exception e) 
            logger.error("handleException", e);
            return null;
        
    

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig 
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) 
        CustomRedisCacheManager redisCacheManager = new CustomRedisCacheManager(redisTemplate);
        redisCacheManager.setUsePrefix(true);
        return redisCacheManager;
    

【讨论】:

这在 Spring Data Redis 2.x 中不起作用,因为 RedisCacheManager 的构造函数不接受 RedisTemplate【参考方案4】:

实际上我的回复是针对@Vivek Aditya 先生 - 我遇到了同样的问题:新的 spring-data-redis api 并且没有为每个 RedisTemplate 构建 RedisCacheManager。唯一的选择——基于@John Blum 的建议——是使用方面。下面是我的代码。

@Aspect
@Component
public class FailoverRedisCacheAspect 

    private static class FailoverRedisCache extends RedisCache 

        protected FailoverRedisCache(RedisCache redisCache) 
            super(redisCache.getName(), redisCache.getNativeCache(), redisCache.getCacheConfiguration());
        

        @Override
        public <T> T get(Object key, Callable<T> valueLoader) 
            try 
                return super.get(key, valueLoader);
             catch (RuntimeException ex) 
                return valueFromLoader(key, valueLoader);
            
        

        private <T> T valueFromLoader(Object key, Callable<T> valueLoader) 
            try 
                return valueLoader.call();
             catch (Exception e) 
                throw new ValueRetrievalException(key, valueLoader, e);
            
        
    

    @Around("execution(* org.springframework.cache.support.AbstractCacheManager.getCache (..))")
    public Cache beforeSampleCreation(ProceedingJoinPoint proceedingJoinPoint) 
        try 
            Cache cache = (Cache) proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
            if (cache instanceof RedisCache) 
                return new FailoverRedisCache((RedisCache) cache);
             else 
                return cache;
            
         catch (Throwable ex) 
            return null;
        
    

适用于所有合理的场景:

应用程序在 redis 关闭时启动正常 应用程序(仍然)在(突然)redis 中断期间工作 当 redis 重新开始工作时,应用会看到它

编辑:代码更像是一个 poc - 仅用于“获取”,我不喜欢每次缓存命中都重新实例化 FailoverRedisCache - 应该有一个映射。

【讨论】:

这对我来说也适用于所有场景。唯一的问题是我必须将 redis 连接超时从默认的 60 秒减少到几秒,否则它会在故障转移之前等待更长时间。【参考方案5】:

所有核心 Spring 框架 Cache abstraction annotations(例如 @Cacheable)以及 JSR-107 JCache annotations supported by the core SF 委托到底层 CacheManager 底层,对于 Redis,这是 @ 987654324@.

你会configure the RedisCacheManager in Spring XML configuration meta-data similar to here。

一种方法是为 (Redis)CacheManager 编写一个 AOP 代理,该代理使用 RedisConnection(间接来自 RedisTemplate)来确定每个 (Redis)CacheManger 操作上的连接状态。

如果连接失败或关闭,对于标准缓存操作,(Redis)CacheManager 可以为 getCache(String name) 返回一个 RedisCache 的实例,该实例始终返回 null(表示条目上的缓存未命中),因此传递到底层数据存储。

也许有更好的方法来处理这个问题,因为我不是所有 Redis(或 SDR)方面的专家,但这应该可以工作,也许会给你一些你自己的想法。

干杯。

【讨论】:

我认为这行不通。由于异常层次结构直到 getCache 方法才显示跟踪。 我尝试覆盖 RedisTemplate 类的执行方法。但是如果发生异常,我不能返回空对象。我必须返回由执行方法返回的泛型类型的对象,因为如果泛型类型是接口或抽象类,则会出现问题。【参考方案6】:

我遇到了同样的问题,但不幸的是,上述解决方案都不适合我。我检查了问题,发现如果没有与 Redis 的连接,执行的命令永远不会超时。所以我开始研究生菜库以寻求解决方案。我通过在没有连接时拒绝命令来解决问题:

@Bean
public LettuceConnectionFactory lettuceConnectionFactory()

    final SocketOptions socketOptions = SocketOptions.builder().connectTimeout(Duration.ofSeconds(10)).build();
    ClientOptions clientOptions = ClientOptions.builder()
            .socketOptions(socketOptions)
            .autoReconnect(true)
            .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
            .build();

    LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(10))
            .clientOptions(clientOptions).build();

    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(this.host, this.port);
    return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);


【讨论】:

【参考方案7】:

您可以使用CacheErrorHandler。但你应该确保 RedisCacheManager transactionAwarefalse 在你的 Redis 缓存配置中(以确保在执行缓存部分时尽早提交事务并且错误被 CacheErrorHandler 捕获并且不要等到跳过 @987654325 的执行结束@ 部分)。将transactionAware 设置为false 的函数如下所示:

    @Bean
    public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) 
        JdkSerializationRedisSerializer redisSerializer = new JdkSerializationRedisSerializer(getClass().getClassLoader());

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(redisDataTTL))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));

        redisCacheConfiguration.usePrefix();

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();

        redisCacheManager.setTransactionAware(false);
        return redisCacheManager;
    

【讨论】:

以上是关于如果 redis 连接失败,如何在运行时禁用 Redis 缓存的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cache with Redis - 如果与 Redis 的连接失败,如何优雅地处理甚至跳过缓存

测试时在spring boot中禁用Redis AutoConfig

Ionic 2 - 如果没有互联网 + 有效表单,我如何禁用按钮

如何在 Django-Celery 失败的情况下设置重试任务

Redis 错误:错误:Redis 连接到 127.0.0.1:6379 失败 - 连接 ECONNREFUSED 127.0.0.1:6379

微服务连接不上redis会直接访问数据库吗