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

jdgnovmf  于 2023-03-30  发布在  Spring
关注(0)|答案(9)|浏览(180)

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

ct3nt3jp

ct3nt3jp1#

让我们把这个问题简单地说一下。你的应用程序使用缓存(用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 core的Caching Abstraction中确定Cache“miss”的所有问题是返回的值为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存在问题,这样你就可以适当地处理异常。但是,如果你在检查时确定异常与连接问题有关,你可以返回“null”来让Spring Caching Infrastructure继续,就像它是一个Cache“miss”一样(即在本例中,坏的Redis连接== Cache miss)。
我知道这样的东西应该可以帮助您解决问题,因为我为GemFire和Pivotal的一个客户构建了一个类似的“自定义”CacheManager实现原型。该高速缓存“未命中”必须由应用程序域对象的“过时版本”触发,其中生产具有通过Spring的Caching Abstraction连接到GemFire的较新和较旧应用程序客户端的混合。例如,应用程序域对象字段在应用程序的较新版本中会发生更改。
无论如何,希望这对你有帮助或给你更多的想法。
干杯!

qcuzuvrc

qcuzuvrc2#

所以,我今天正在挖掘核心Spring Framework Caching Abstraction源代码,解决另一个问题,似乎如果CacheErrorHandler正确实现,那么可能有问题的Redis Connection仍然会导致所需的行为,例如缓存“未命中”(由返回null值触发)。
有关更多详细信息,请参见AbstractCacheInvoker源代码。
cache.get(key)应该会由于Redis连接错误而导致异常,因此会调用Exception处理程序...

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

如果CacheErrorHandler正确处理了Cache“get”错误(并且没有重新抛出/an Exception),则将返回一个空值,指示缓存“miss”。

nukf8bse

nukf8bse3#

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

k3fezbri4#

实际上,我的回应是针对@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重新开始工作时,app会看到它

编辑:代码更像是一个poc --只用于“get”,我不喜欢在每次缓存命中时都重新示例化FailoverRedisCache--应该有一个Map。

unftdfkk

unftdfkk5#

在使用Sping Boot 2.3.9.release和Redis时,上述方法都不适用。我们最终创建并注册了自己的自定义CacheErrorHandler,名为CustomCacheErrorHandler,以覆盖Spring Framework提供的默认SimpleCacheErrorHandler。这将完美地工作。

@Configuration
  public class CachingConfiguration extends CachingConfigurerSupport {  
    @Override
    public CacheErrorHandler errorHandler() {
        return new CustomCacheErrorHandler();
    }
  }

  class CustomCacheErrorHandler implements CacheErrorHandler {
    Logger log = Logger.get(CustomCacheErrorHandler.class);

    @Override
    public void handleCacheGetError(RuntimeException e, Cache cache, Object o) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCachePutError(RuntimeException e, Cache cache, Object o, Object o1) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCacheEvictError(RuntimeException e, Cache cache, Object o) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCacheClearError(RuntimeException e, Cache cache) {
        log.error(e.getMessage(), e);
    }
  }
ljsrvy3e

ljsrvy3e6#

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

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

}
6ss1mwsb

6ss1mwsb7#

所有核心 Spring FrameworkCache抽象注解(例如@Cacheable)沿着核心SF支持的JSR-107 JCache注解都委托给底层的CacheManager,对于Redis,就是RedisCacheManager
您将在Spring XML配置元数据中配置RedisCacheManager,类似于此处。
一种方法是为(Redis)CacheManager编写一个AOP代理,它使用RedisConnection(间接来自RedisTemplate)来确定每个(Redis)CacheManager操作的连接状态。
如果连接失败或关闭,对于标准缓存操作,(Redis)CacheManager可以返回一个RedisCache的示例,用于getCache(String name),该示例总是返回null(表示条目上的缓存未命中),从而传递到底层数据存储。
也许有更好的方法来处理这个问题,因为我不是一个Maven的所有事情Redis(或SDR),但这应该工作,也许给予你一些想法自己。
干杯。

pes8fvy9

pes8fvy98#

Spring提供了内置的实现,用于在该高速缓存提供者服务器(例如:Redis)不可达或访问该高速缓存时发生运行时异常,只需使用LoggingCacheErrorHandler将异常记录为警告即可。
默认情况下Spring auto configuresCacheErrorHandlerof typeSimpleCacheErrorHandler,它只是抛出异常。所以为了避免抛出异常-〉我们需要做的就是使用以下配置覆盖cacheErrorHandler:

@Configuration
public class CachingConfiguration extends CachingConfigurerSupport {
  @override
  public CacheErrorHandler errorHandler() { return new LoggingCacheErrorHandler(); }
}

这将确保该高速缓存失败异常不会再传播或使您的应用程序或API失败。同时,您将这些异常记录为警告,我们可以根据这些警告采取行动。

注意:我已经用 spring-boot 2.7.10 测试过了

sczxawaw

sczxawaw9#

你可以使用CacheErrorHandler。但是你应该确保在Redis缓存配置中将RedisCacheManager transactionAware设置为false(确保事务在执行缓存部分时提前提交,并且错误被CacheErrorHandler捕获,并且不要等到执行结束时才跳过CacheErrorHandler部分)。将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;
    }

相关问题