Как отключить кэширование Redis во время выполнения, если соединение redis не выполнено

У нас есть приложение api для отдыха. Мы используем redis для кэширования ответов API и внутреннего кэширования методов. Если redis-соединение, это приведет к сокращению нашего API. Мы хотим обойти кэширование redis, если это соединение redis завершится неудачей или какое-либо исключение, вместо того, чтобы снизить наш API. Существует интерфейс CacheErrorHandler, но он обрабатывает ошибки redis get set, а не проблемы с повторным подключением. Мы используем Spring 4.1.2.

Ответ 1

Пусть это немного сварится. В вашем приложении используется кеширование (реализовано с помощью Redis). Если соединение Redis устарело/закрыто или в противном случае, вы хотите, чтобы приложение обходило кеширование и (предположительно) переходило непосредственно в базовое хранилище данных (например, RDBMS). Логика приложения Service может выглядеть так же, как...

@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 ядре Caching Abstraction для определения "промаха" кэша, заключается в том, что возвращаемое значение равно null. Таким образом, Spring Инфраструктура кэширования будет действовать при вызове фактического метода службы (т.е. GetCustomer). Имейте в виду, что при возврате вызова getCustomerRepo(). Load (customerId) вам также необходимо обработать случай, когда Spring Кэширующая инфраструктура пытается теперь кэшировать значение.

В духе поддержания простоты, мы обойдемся без АОП, но вы также сможете достичь этого, используя АОП (на ваш выбор).

Все, что вам нужно (должен) - это "настраиваемый" RedisCacheManager, расширяющий SDR CacheManager реализацию, что-то вроде...

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, чтобы исключить Exception, а затем хорошо знаю, что проблема с Redis существует, и чтобы вы могли правильно справиться с Exception, Однако, если вы уверены, что Исключение связано с проблемой подключения при проверке, вы можете вернуть "null", чтобы Spring Кэширующая инфраструктура продолжалась, как если бы это была ошибка "Ошибка" (т.е. Плохое Redis Connection == Cache пропустите, в этом случае).

Я знаю, что что-то вроде этого должно помочь вашей проблеме, поскольку я построил аналогичный прототип "пользовательской" реализации CacheManager для GemFire ​​и одного из клиентов Pivotal. В этом конкретном UC кэш "пропустить" должен был быть вызван "устаревшей версией" объекта домена приложения, где в производстве было сочетание более старых и старых приложений, подключающихся к GemFire ​​через Spring Caching Abstraction. Например, поля объектов домена приложения будут меняться в новых версиях приложения.

В любом случае, надеюсь, что это поможет или даст вам больше идей.

Ура!

Ответ 2

Итак, я копал ядро ​​ Spring Framework Caching Abstraction source сегодня, обращаясь к другому вопросу, и кажется, что если CacheErrorHandler реализован правильно, возможно, проблемное соединение Redis Connection может привести к желаемому поведению, например. cache "miss" (срабатывает с возвратом нулевого значения).

Подробнее см. AbstractCacheInvoker.

cache.get(key) должен привести к исключению из-за неисправности Redis Connection и, следовательно, обработчик Exception будет вызван...

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

Если CacheErrorHandler правильно обрабатывает ошибку "получить" кэша (и не перебрасывает/исключение), тогда возвращается нулевое значение, указывающее на "промах" кеша.

Ответ 3

Спасибо @Джон Блюм. Мое решение в 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;
    }
}

Ответ 4

на самом деле мой ответ направлен г-ну @Vivek Адитья - я столкнулся с той же проблемой: новый Spring-data-Redis API, а не создание RedisCacheManager для RedisTemplate. Единственный вариант - основанный на предложениях @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 Down
  • приложение (все еще) работает во время (внезапного) отключения Redis
  • когда Redis начинает работать снова, приложение видит это

Редактировать: код больше похож на poc - только для "get", и мне не нравится повторное создание FailoverRedisCache при каждом попадании в кеш - должна быть карта.

Ответ 5

Все основные Spring Framework Аннотации кэш-абстракции (например, @Cacheable) вместе с JSR-107 JCache аннотации, поддерживаемые ядром SF, делегируют базовую CacheManager под капотом, и для Redis, то есть RedisCacheManager.

Вы бы сконфигурировали RedisCacheManager в метаданных конфигурации Spring XML, похожих на здесь.

Одним из подходов было бы написать AOP-прокси для (Redis) CacheManager, который использует RedisConnection (косвенно из RedisTemplate), чтобы определить состояние соединения для каждой операции (Redis) CacheManger.

Если соединение не выполнено или закрыто для стандартных операций кэширования, (Redis) CacheManager может вернуть экземпляр RedisCache для getCache (имя строки), который всегда возвращает значение null (указывая на пропущенную кэш-память), таким образом, проходя через базовое хранилище данных.

Там могут быть лучшие способы справиться с этим, поскольку я не эксперт по всем вещам Redis (или SDR), но это должно сработать и, возможно, дать вам несколько своих собственных.

Приветствия.

Ответ 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);

}