如果 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 transactionAware
到 false
在你的 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