[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)
- redis-cli를 통해 정상 동작 중인지 테스트해볼 수 있다.
- redis-cli( -h localhost -p 6379)
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<String, String> reactiveRedisTemplate() {
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisSerializationContext<String, String> serializationContext = RedisSerializationContext.<String, String>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<RedisModel> redisModelList = new ArrayList<>();
public Mono<RedisModel> 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<Void> 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<String, String> reactiveRedisTemplate;
@Around(value = "execution(public * com.dongdd.redis_example.service.LongtimeService.getRedisModel(..))")
public Mono<RedisModel> 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<RedisModel>) 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