我们有 rest api 应用程序。我们使用 redis 进行 API 响应缓存和内部方法缓存。如果 redis 连接,那么它会使我们的 API 关闭。如果 redis 连接失败或出现任何异常,我们希望绕过 redis 缓存,而不是关闭我们的 API。有一个接口 CacheErrorHandler 但它处理 redis 获取设置操作失败而不是 redis 连接问题。我们使用的是 Spring 4.1.2。
7 回答
让我们稍微简化一下。您的应用程序使用缓存(使用 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 核心的缓存抽象中确定缓存“未命中”的所有重要因素是返回的值为空。因此,Spring Caching Infrastructure 将继续调用实际的 Service 方法(即 getCustomer)。请记住在 getCustomerRepo().load(customerId) 调用的返回上,您还需要处理 Spring 的 Caching Infrastructure 现在尝试缓存该值的情况。
本着保持简单的精神,我们将不使用 AOP,但您也应该能够使用 AOP 来实现这一点(您的选择)。
您(应该)需要的只是一个扩展SDR CacheManager 实现的“自定义” 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 框架缓存抽象源来解决另一个问题,如果 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”错误(并且不重新抛出/异常),则将返回一个空值,指示缓存“未命中”。
谢谢@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;
}
}
实际上我的回复是针对@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 - 应该有一个映射。
所有核心Spring Framework Cache 抽象注释(例如 @Cacheable)以及由核心 SF 委托支持的 JSR-107 JCache 注释到底层CacheManager的底层,对于 Redis,即RedisCacheManager。
您将在类似于此处的 Spring XML 配置元数据中配置 RedisCacheManager。
一种方法是为 (Redis)CacheManager 编写一个 AOP 代理,该代理使用RedisConnection(间接来自RedisTemplate)来确定每个 (Redis)CacheManger 操作的连接状态。
如果标准缓存操作的连接失败或关闭,则 (Redis)CacheManager 可以为getCache(String name)返回一个RedisCache实例,该实例始终返回 null(表示条目上的缓存未命中),从而传递到底层数据存储。
可能有更好的方法来处理这个问题,因为我不是 Redis(或 SDR)所有方面的专家,但这应该可以工作,也许会给你一些你自己的想法。
干杯。
我有同样的问题,但不幸的是,上述解决方案都不适合我。我检查了问题,发现如果没有连接到 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);
}
您可以使用CacheErrorHandler
. 但是你应该确保
RedisCacheManager transactionAware
在false
你的 Redis 缓存配置中进行(以确保在执行缓存部分时尽早提交事务并且错误被捕获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;
}