DongDD's IT

[Spring] Spring Data Redis - Cache 본문

프로그래밍/Spring

[Spring] Spring Data Redis - Cache

DongDD 2019. 12. 21. 14:11

[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<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

 

DongDDo/spring_exam

sping_exam. Contribute to DongDDo/spring_exam development by creating an account on GitHub.

github.com

 

 

Reference


http://redisgate.jp/redis/configuration/redis_conf_list.php

https://spring.io/guides/gs/spring-data-reactive-redis/

Comments