Redis Cache

뾰족머리삼돌이·2024년 10월 10일
0

Spring Data Redis

목록 보기
3/12

Redis Cache

Spring Data Redis는 org.springframework.data.redis.cache 패키지를 통해 Spring 프레임워크의 캐시 추상화를 제공한다.

버퍼와 캐시는 유사한 뜻으로 받아들여지는 경우가 많다.

버퍼는 서로 다른 처리시간을 가진 객체사이에서 특정 크기이상의 덩어리로 이동시키기 위한 임시저장소를 뜻한다.
반면, 캐시는 동일한 데이터를 반복적으로 읽을 때 성능향상을 위한 저장소로 사용된다.

RedisCacheManager를 설정파일에 추가함으로써 캐시관련 설정이 가능하며, Spring Boot에서는 RedisCacheConfiguration에서 자동설정을 제공한다.

RedisCacheManager의 세부설정은 RedisCacheManagerBuilder를 통해 작성할 수 있다.

RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    .transactionAware()
    .withInitialCacheConfigurations(Collections.singletonMap("predefined",
        RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
    .build();

RedisCacheManager에 의해 생성된 RedisCache의 동작은 RedisCacheConfiguration와 함께 정의된다.
또한, 아래 예시처럼 RedisCacheConfiguration키의 만료시간, 접두사, RedisSerializer 구현을 설정할 수 있다.

RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofSeconds(1))
    .disableCachingNullValues();

캐시 작성과 Lock

public static RedisCacheManager create(RedisConnectionFactory connectionFactory) {

	Assert.notNull(connectionFactory, "ConnectionFactory must not be null");

	RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
	RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

	return new RedisCacheManager(cacheWriter, cacheConfiguration);
}

RedisCacheManager는 기본적으로 처리량의 향상을 위해 바이너리 데이터를 읽고 쓰는데 lock-free RedisCacheWriter 를 사용한다.

Lock-Free 란?

멀티 스레드 환경에서 공유자원 점유와 관련하여 Lock이 사용된다.
단순하게 말하면 Lock을 획득한 스레드에 한정하여 특정 자원에 접근할 수 있는 권한을 지니게 되는 형식이다.
Lock이 너무 많아지게 되면 Lock을 획득하지 못해 작업을 대기하는 스레드가 생기는 문제가 생긴다.

이러한 기아현상을 방지하기 위해 Mutex나 Semaphore 처럼 공유자원을 관리할 수 있는 기법이 사용된다.
하지만, 근본적으로 Lock을 설정하고 해제하는 작업 모두 시스템 자원을 사용하기에 성능에 영향을 끼치며, 특정 스레드가 Lock을 취득하고 있는 동안 자원에 접근할 수 없는 Blocking 상태가 생기기도 한다.

Lock-Free는 이러한 Blocking이 없는 Non-Blocking 알고리즘의 등급에 해당한다.
자원에 접근하는 여러 스레드 중에서 적어도 하나의 스레드는 유한한 단계에 실행을 끝마치게 되는 구현방식을 의미한다
즉, Lock을 사용하지 않고 적어도 하나의 스레드는 작업에 성공하도록 하는 것이다.

이를 구현하기위해 시스템 수준에서 원자성을 보장하는 atomic 연산인 CAS 알고리즘이 사용된다.

Lock을 사용하지 않기때문에 여러스레드가 동시에 캐시자원에 접근할 수 있고, 이는 중복되거나 비원자적인 실행을 초래할 위험이 있다.

따라서, 성능적인 부분을 고려하여 Lock을 사용하도록 설정하는 것을 고민할 필요가 있다.
Lock 동작을 선택하고 싶다면 아래와 같이 작성하면 된다.

RedisCacheManager cacheMangager = RedisCacheManager
    .build(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) // lock 사용
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

저장되는 lock key는 기본적으로 {캐시 명}::{데이터 키}의 형태로 저장되며, 이는 설정을 통해 변경할 수 있다.

RedisCacheManager cacheManager = RedisCacheManager
//	.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.keys()))
    .build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

기본적으로 RedisCacheManagerKEYS와 DEL전략을 사용하여 캐시를 초기화한다. ( redis-cli의 keys 명령 )
KEYS는 Redis 데이터베이스의 전체 키 공간을 탐색하기 때문에 성능문제가 있을 수 있다.

따라서, 기본 배치전략을 SCAN으로 사용하여 적절한 배치 사이즈를 설정해주는 것이 권장된다.
( 단, SCAN 전략은 Lettuce 드라이버 사용 시에만 완벽하게 지원된다. )

캐시 만료기간

캐시 만료기간과 관련해서는 TTL( Time-To-Live )와 TTI( Time-To-Idle ) 에 대한 이해가 필요하다.

TTL은 특정 데이터가 유효한 고정시간을 의미한다.
이 시간은 초기 데이터접근이나 수정과정에서 설정 및 재설정되며, 정해진 만료기간이 경과되기 이전에 수정작업이 이뤄지면 다시 해당 시간만큼 갱신된다. 저장소에따라 TTL을 갱신없이 고정시간 이후 만료시키기도 한다.

TTI는 특정 데이터가 비활성 상태로 남아있을 수 있는 시간을 의미한다.
데이터를 읽거나 수정할 때마다 갱신된다.

TTL 만료기간

RedisCacheConfiguration fiveMinuteTtlExpirationDefaults =
    RedisCacheConfiguration.defaultCacheConfig().enableTtl(Duration.ofMinutes(5));
    
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
    .cacheDefaults(fiveMinuteTtlExpirationDefaults)
    .build();

모든 캐시들이 공통적으로 특정 시간 이후에 만료되어야 한다면 위 코드처럼 작성하면 된다.


enum MyCustomTtlFunction implements TtlFunction {

    INSTANCE;

    @Override
    public Duration getTimeToLive(Object key, @Nullable Object value) {
        // compute a TTL expiration timeout (Duration) based on the cache entry key and/or value
    }
}
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(MyCustomTtlFunction.INSTANCE);

RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
    .cacheDefaults(defaults)
    .build();

캐시별로 만료기간이 상이하게 설정하고 싶다면, 위 코드들처럼 RedisCacheWriter.TtlFunction 인터페이스의 사용자 정의 구현을 제공하면 된다.

TTI 만료기간

Redis 자체는 TTI 만료개념을 지원하지 않지만, Spring Data Redis에서는 유사한 동작을 구성할 수 있다.

@Configuration
@EnableCaching
class RedisConfiguration {

    @Bean
    RedisConnectionFactory redisConnectionFactory() {
        // ...
    }

    @Bean
    RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(5))
            .enableTimeToIdle();

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaults)
            .build();
    }
}

enableTimeToIdle()을 통해 명시적으로 TTI 구성을 활성화시키고, 앞선 TTL 만료기간 설정을 작성해야한다.

Redis의 TTL만료가 키의 값을 읽을 때 동작하는 점을 이용하여 만료시간을 재설정 하는 방식으로 동작한다.
Redis 6.2.0 버전부터 존재하는 GETEX 명령을 통해 데이터를 조회가 성공하면 만료기간을 갱신하는 것이다.

Spring Data Redis 애플리케이션에서 다양한 데이터 접근 패턴을 혼합하여 사용하는 경우, TTL 만료가 설정된 항목을 읽는다고 반드시 만료되지 않는 것은 아니다.

예를들어, TTL 만료기간과 함께 @Cacheable 서비스 메서드를 호출하여 데이터를 작성헀다고 가정해보자. Spring Data Redis Repository에서GET명령을 사용하여 데이터를 읽으면 만료기간이 갱신되지 않는다.

참고 자료

0개의 댓글

관련 채용 정보