[Spring] Spring Data Redis - Cache
[Spring] Spring Data Redis - Cache
[Spring] Spring Data Redis - Cache
Redis(Remote Dictonary server)
- Inmemory key-value 데이터베이스이며 NoSQL DBMS이다.
- Message queue, Shared Memory, Remote dictionary 용도로 사용된다고 한다.
- Key-value 저장소이고 Inmemory이기 때문에 좋은 성능을 제공하여 Cache 용도로 사용한다.
-> Redis reference : http://redisgate.jp/redis/configuration/redis_conf_list.php (일본 사이트 인 것 같은데 한글로 번역되어 있다)
- Redis server는 single-thread로 동작한다.
Local에 Redis server 띄우기
- Docker hub에 redis docker image를 사용하여 쉽게 redis server를 띄워볼 수 있다.
- Dockerfile
FROM redis COPY redis.conf /usr/local/etc/redis/redis.conf CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]
- redis.conf 적용(requirepass로 password 적용)
daemonize no pidfile /var/run/redis/redis-server.pid port 6379 loglevel notice maxclients 10000 maxmemory 1024m maxmemory-policy volatile-lru maxmemory-samples 3 requirepass test
- 설정 정보에 대해서는 위의 redis link에서 찾아볼 수 있다.
- 6379 포트에 바인딩하여 run(docker run -p 6379:6379 redis_test)
docker run redisimage
- redis-cli를 통해 정상 동작 중인지 테스트해볼 수 있다.
- redis-cli( -h localhost -p 6379)
redis-cli로 접속
Spring Data Redis
- Spring에서 채택한 client library로는 Jedis, Lettuce 두개가 있다.
- Jedis는 Java 기반으로 만들어진 library이고 Lettuce는 netty 기반으로 만들어졌기 때문에 여러 특징들을 고려하여 Spring에서는 lettuce를 사용하는 것 같다.
-> jedis는 thread-safe 하지 않다.(connection pool을 사용하여 multi thread 상황을 해결해야 한다)
-> lettuce는 multiple-thread 상황에서 safe하다.
- lettuce를 사용한 예제(reactive)만 다룰 예정이다.
Spring Redis Cache Example
- 보통 Database같은 data 조회에 오랜 시간을 소요해야할 때, redis cache를 사용한다.
- 예제에서는 database 대신 긴 delay를 준 service를 두고 해당 service 접근할 때 캐시를 사용하도록 구현하였다.
1. Redis Configuration
@Configuration public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port}") private int redisPort; @Value("${redis.password}") private String password; @Bean public LettuceConnectionFactory lettuceConnectionFactory() { LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder() .commandTimeout(Duration.ofMinutes(1)) .shutdownTimeout(Duration.ZERO) .build(); RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost, redisPort); redisStandaloneConfiguration.setPassword(password); return new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration); } @Bean public ReactiveRedisTemplate reactiveRedisTemplate() { StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); RedisSerializationContext serializationContext = RedisSerializationContext.newSerializationContext(stringRedisSerializer) .value(stringRedisSerializer) .build(); return new ReactiveRedisTemplate<>(lettuceConnectionFactory(), serializationContext); } }
- redis host/port/passwrod는 application.yml 파일에 정의되어 있다.
- LettuceConfiguration 생성
commandTimeout : connection time out이라고 보면 될 것 같다.
shutdownTimeout : redis client가 graceful하게 close될 때까지의 timeout 설정(0일 경우, 제한을 두지 않는다)
- RedisCluster도 있지만 실제 구현에서 단일 인스턴스로 cache를 구현할 것이기 때문에 RedisStandaloneConfiguration을 사용했다.
- String key/value를 사용하기 위해 serializer 등록
2. LongtimeService
- cache test를 위해 5초간 딜레이를 주는 service 생성하였다.(보통 database 접근과 같이 오래 걸리는 작업에 cache를 둔다)
@Service public class LongtimeServiceImpl implements LongtimeService { private List redisModelList = new ArrayList<>(); public Mono getRedisModel(String name) { return Mono.just(redisModelList) .map(redisModels -> redisModels.stream().filter(redisModel -> redisModel.getName().equals(name)) .findFirst()) .filter(Optional::isPresent) .map(Optional::get) .switchIfEmpty(Mono.empty()) .delayElement(Duration.ofSeconds(3)); } public Mono putRedisModel(RedisModel redisModel) { return Mono.just(redisModel) .filter(r -> StringUtils.isNotEmpty(r.getName())) .doOnNext(r -> redisModelList.add(r)) .then(); } }
- RedisModel : name(String)/message(String)
- RedisModel을 저장하는 리스트를 만들고 put 호출 시 저장하고 get 호출 시 파라미터로 받은 name과 일치하는 데이터가 존재하면 5초간 기다린 후 데이터를 푸시한다.
3. RedisAspect
@Aspect @Component public class RedisAspect { @Autowired private ReactiveRedisTemplate reactiveRedisTemplate; @Around(value = "execution(public * com.dongdd.redis_example.service.LongtimeService.getRedisModel(..))") public Mono redisModelAspect(ProceedingJoinPoint proceedingJoinPoint) { String name = (String) proceedingJoinPoint.getArgs()[0]; return CacheMono .lookup(k -> reactiveRedisTemplate.opsForValue() .get(name) .map(message -> RedisModel.builder().name(name).message(message).build()) .map(Signal::next), name) .onCacheMissResume(() -> { try { return (Mono) proceedingJoinPoint.proceed(); } catch (Throwable throwable) { return Mono.error(throwable); } }) .andWriteWith((k, sig) -> Mono.fromRunnable(() -> Optional.ofNullable((RedisModel) sig.get()) .filter(redisModel -> StringUtils.isNotEmpty(redisModel.getName())) .ifPresent(redisModel -> reactiveRedisTemplate.opsForValue() .set(redisModel.getName(), redisModel.getMessage()).subscribe()))); } }
- 먼저 redis cache를 확인한다.(reactiveRedisTemplate.opsForValue().get(name))
- cache가 존재할 경우 바로 return을 해주고 아니라면 OnCacheMissResume으로 넘어간다.
- OnCacheMissResume에서는 aspect의 다음 행동 즉, LongtimeService에서 동작을 하고 해당 데이터를 받는다.
- andWriteWith에서는 Service를 호출하고 받은 데이터가 존재한다면 redis에 넣어준다.(reactiveRedisTemplate.opsForValue().set(key, value, [expired_time])
set할 때, 3번째 인자로 expired time을 설정할 수 있다.
- client는 데이터를 받게 된다.
- 이후 요청 시, lookup에서 redis cache가 존재한다면 OnCacheMissResume/andWriteWith는 동작하지 않으므로 딜레이를 기다리지 않고 바로 data를 return해 줄 수 있다.
4. 동작
실제 적용하면서 겪었던 이슈
1. Lettuce에서 간헐적으로 발생하는 CommandTimeoutException
- 결론부터 말하자면 이 원인은 해결하지 못했다.
- Lettuce library를 사용하면서 가끔씩 CommandTimeoutException이 발생하는 경우가 있었다.
- 어떤 상황에서 발생하는지 파악하기가 어려웠고 재현하기도 어려워 이 문제를 해결하지는 못했으나 유실되도 상관없는 cache용도로 사용했기 때문에 다른 대책이 없어 사용하기로 결정했다.
- 해당 이슈를 해결해보려 찾아보다가 lettuce github issue에서 같은 문제를 발견하였으나 발생하는 원인이 다른 것 같다고 판단하게 되었다.
-> https://github.com/lettuce-io/lettuce-core/issues/817
2. Spring Redis Cache의 reactive 지원
- ReactiveRedisTemplate만으로 구현해도 상관없으나 Cache 관리를 위해 Spring RedisCache를 사용하고 싶었다.
- Lettuce에서는 reactive를 지원해주나 spring에서 지원해주는 cache에서는 reactive로 정상 동작하지 않는 것 같았다.
해결책
Spring RedisCache를 사용하는 대신 Cache를 새로 implement하여 ReactiveRedisTemplate을 사용하는 Object를 만들어 해당 RedisCache를 사용하여 구현하여 해결하였다.
Github Code
https://github.com/DongDDo/spring_exam/tree/feature/RedisExample
Reference
http://redisgate.jp/redis/configuration/redis_conf_list.php
https://spring.io/guides/gs/spring-data-reactive-redis/
from http://dongdd.tistory.com/182 by ccl(A) rewrite - 2020-03-06 04:20:47
댓글
댓글 쓰기