본문 바로가기

개발기술/데이터베이스

Redis 사용법 (Redis환경설정, Spring Redis)

캐쉬전략 고려요소

전체 DB 데이터의 10%~20% 정도만 Redis에 캐싱
→ 자주 조회되는 데이터만 캐싱하고, 나머지는 DB에서 조회하는 것이 일반적

DB 대비 Redis 저장 비율을 결정하는 요소

요소설명캐시 저장 비율 영향

데이터 액세스 패턴 자주 조회되는 데이터인지? 빈번한 조회 데이터일수록 캐싱 비율 높여야 함
데이터 크기 캐싱할 데이터가 크다면? 큰 데이터는 캐싱 비율을 낮춰야 함 (RAM 절약)
데이터 변경 빈도 자주 변경되는 데이터인가? 변경이 많으면 캐싱 비율 낮춰야 함
TTL(Time To Live) 캐시 만료 시간을 짧게 설정? 짧을수록 캐싱된 데이터 양이 줄어듦
DB 부하 DB가 병목인가? DB 부하가 크면 Redis 캐싱 비율을 높여야 함

 

 Redis 캐슁전략

LRU는 "오래 사용되지 않은 데이터"를 삭제 → 최신 데이터가 중요할 때 적합
LFU는 "자주 사용되지 않은 데이터"를 삭제 → 조회 빈도가 중요한 경우 적합

레디스 

  • DB의 디스크가 아닌 메모리에 저장하여 휘발성이 있지만 영속성 기능을 지원하기도 함. 안정성이 비교적 떨어지기 때문에주로 캐시서버로 사용됨.
  • 인덱스방식이 아니라 key-value방식으로 저장
  • 다양한 방식의 데이터타입 적용가능 (hash나 list같은 데이터 저장가능)
  • tomcat과는 다르게 embedded방식이 아니라 OS내에 설치하여 별도의 서버로 동작한다.(micro service)

 

레디스 종류

싱글인스턴스방식 

센티넬, 클러스터방식 - 마스터 슬레이브 구조을 하여, 서버 한대가 죽더라도 다른 서버가 살아있으면 구동 가능하도록 구현

 

레디스 활용점

1. 캐쉬서버로서 DB로의 직접접근을 최소화시킴

2. 동기화 문제를 해결하기 위해서 DB의 Serializable을 대신해서 SpinLock을 구현

 

OS 내에 레디스 설치 및 운용

레디스 설치 : https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-mac-os/

레디스 문서 : http://redisgate.kr/redis/introduction/redis_intro.php

 

맥의 경우 homebrew를 통해서 redis를 설치(brew install redis)한 후에, Redis서버를 동작시켜놓아야 Spring이 Lettuce or Jedis 같은 Client를 통해서 redis전용 프로토콜을 사용하여 레디스 서버와 통신할 수 있다. CLI에서는 한개의 콘솔창에서 서버가 띄워져있어야 다른 콘솔창에서 Client로써 접근이 가능하다. 

 

  • CLI를 통한 레디스 제어 명령어
    • redis-server : Redis 서버를 실행하고 관리합니다
    • redis-cli : Redis 서버에 연결하고 명령을 입력할 수 있는 클라이언트입니다.
    • ping : Client서버에서 Server와 연결되었는지 확인할 명령어, 연결되어있다면 pong으로 회신이 온다.
    • set myKey myValue : key - value pair를 저장함
    • get myKey : key를 통해 value를 찾음
    • del myKey : key를 통해 key-value pair를 제거함
    • keys * : 모든 key를 조회한다.
    • ctrl + c : redis 서버를 종료한다.
    • 레디스를 CLI를 통해서 동작시키면 CMD창이 꺼지면 같이 꺼지기때문에 CMD로 시작하기보다는 brew를 통해서 실행 및 종료를 시켜야함.
      • brew services start redis
      • brew services stop redis

레디스

 레디스는 하나의 컴퓨터에서도 포트를 바꾸면 여러대 띄울 수 있다. 포트는 레디스 환경설정에서 변경가능

  • 레디스 환경설정
    • vi /user/local/etc/redis.conf : Redis환경설정 변경을 위해서 파일 열기
      • /requirepass : 'requirepass'키워드로 문서를 검색하여 'foobared'로 표기된 password 설정위치 확인
      • /port : 'port'키워드로 검색하여 6379로 설정되어있는 값을 변경가능
    • redis-server /user/local/etc/redis.conf : 변경된 환경설정으로 서버실행

레디스 클라이언트

Redis에는 다양한 인기 클라이언트가 있으며, 각 클라이언트는 다양한 사용 사례, 선호도, 성능 요구에 맞게 설계되었습니다.

 

Lettuce (Java)

  • 특징: 고성능, 경량 클라이언트로 동기, 비동기 및 리액티브 프로그래밍 모델을 지원합니다.
  • 사용 사례: 기본 캐싱, 데이터 저장, Pub/Sub에 적합하며, 주로 Spring의 CacheManager와 함께 캐싱 애플리케이션에서 사용됩니다.

Jedis (Java)

  • 특징: 간단한 동기식 Redis 클라이언트로 기본적인 Redis 작업에 적합합니다.
  • 사용 사례: 간단한 API로 복잡한 설정이 필요 없는 기본적인 Redis 기능을 사용하는 애플리케이션에 주로 사용됩니다.

Redisson (Java)

  • 특징: 분산 락, 데이터 구조, 동기화 도구 및 캐싱을 지원하는 고급 Redis 클라이언트입니다.
  • 사용 사례: 공유 리소스, 분산 락 및 복잡한 Redis 기반 데이터 구조를 필요로 하는 분산 시스템에 적합합니다.

 

Lettuce를 사용한 CacheManager 도입

1. Gradle Implementation

Spring의 의존성으로 redis를 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

2. 설정값 셋팅

Redis 서버 연결 정보 제공: host와 port 설정을 통해 애플리케이션이 Redis 서버가 어디에 위치해 있는지 알 수 있도록 합니다. 해당값을 configuration bean등록시 사용함.

data:
  redis:
    host: localhost
    port : 6379

 

3. Spring Configuration

@EnableCaching을 통해서 Caching관련된 annotation을 자동으로 Scan 하도록 설정한다.

@SpringBootApplication
@EnableCaching
public class ZerobaseReservationApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZerobaseReservationApplication.class, args);
    }
}

 

4. Bean 등록

레디스는 In Memory DB이지만 사용처는 캐쉬, 영속성 DB, 분산락, 메세지 채널 등 여러가지 사용처로 사용될 수 있다. 각 사용처마다 최적화된 기능 설정을 위해서 Configuration을 달리하며 이를 위한 객체들이 필요하다. 레디스를 캐시로 동작시키기 위해서는 대표적으로 두가지 객체가 필요하다. 1. ReddisConnectionFactory 2. ReddisTemplate or ReddisCacheManager

  •  ReddisConnectionFactory : IP와 Port를 설정하여 Lettuce (  Client API, 레디스와 통신할 수 있는 라이브러리)를 사용하여 Redis와 통신하도록 연결을 지원한다. 해당 객체는 ReddisTemplate 혹은 CacheManager 내부에서 사용된다. 
  • ReddisCacheManager :  @Cacheable, @CacheEvict, or @CachePut 과 같은 annotation을 사용하여 간편하고 추상화된 방식으로 스프링에서 캐쉬 기능을 지원한다. 내부적으로 Redistemplate을 사용한다.
    • Seriazliation은 JavaObject와 data를 byte화 시켜서 Java 외부에서 데이터를 인식할 수 있도록 하는 것 
    • redis는 단순한 데이터 외에도 Java Object를 저장할 수 있기때문에 상황에 맞는 적절한 Serialization 전략을 세워주어야한다. Java Object를 Json으로 변경시켜서 Java 외에도 다른 언어에서 받아서 사용할 수 있도록 redis에 표준화된 serialization 전략을 세워줘야함.
    • reddisCacheManager은 미리 지정된 Serialization 전략만 사용할 수 있으며, 만약 다양한 serialization 전략을 사용하고 싶다면 versatile한 serializer을 사용하거나, case by case로 redistemplate을 통해서 정의해주어야한다.
  • RedisTemplate :  Redis를  직접적으로 조작하는데 사용되며, Key-value 등록 등 보다 구체적인 조작을 가능하도록 한다. 주로 단순 annotaion을 사용한 key-valu 캐슁 작업보다는 Geospatial data, 원자적 연산 및 분산락, transactions, Lua scripting, or pub/sub 등의 레디스 기능들을 직접적으로 사용하고자 할때 사용된다.
@RequiredArgsConstructor
@Configuration
public class CacheConfig {
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Bean // redis와 connection을 맺는 bean을 생성
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();
        /* 클러스터 환경으로 설정할 경우 아래의 설정값 사용
        RedisClusterConfiguration redisClusterConfiguration =
         new RedisClusterConfiguration();
         */
        conf.setHostName(host);
        conf.setPort(Integer.parseInt(port));
        // conf.setPassword(); password설정필요시 사용
        return new LettuceConnectionFactory(conf);
    }

    @Bean // redis Connection을 캐쉬에 적용할 수 있는 bean을 생성
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(conf)
                .build();
    }
}

 

4. 원하는 메소드에 캐쉬 적용하기 : @Cacheable(key : inputParameter, value) 

여기서 key와 value는 레디스의 key value랑은 다름. key 값의 prefix를 value라고 칭함.

  • @Cacheable: 해당 메소드가 실행될때 우선은 캐쉬서버에서 값이 있는 지 확인하도록 한다. 값이 없다면, 해당 메소드를 실행후 결과값을 캐쉬 버켓에 저장한다.
  • value = "finance": 해당 값은 키-값 쌍인 캐쉬 메모리의 특정 버켓을 지칭한다. 
  • key = "#companyName": value라는 특정 버켓 내에서 검색할 key값을 지칭한다.  method에 지정한 key와 value가 동일하면 같은 버켓을 조회하여 데이터를 반환한다.
@Cacheable(key = "#companyName", value = "finance")
public ScrapedResult getDividendByCompanyName(String companyName) {

 

5. 캐쉬를 적용하기 위해서 Serialization 에러 해결하기 ; config에서 cover못한 serialization 과정을 별도로 정정

public class DividendInfo {
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime date;

 

6.  캐쉬삭제

  • Config에서 TTL(Time to Live; 유효기간)을 설정하여 삭제주기 설정 ; 특정 prefix value값에 대해서만 적용하도록 설정가능
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofDays(1));

 

  • Scheduler를 사용하여 캐쉬삭제 로직 생성 ; 특정 prefix value값에 대해서만 적용하도록 설정
@CacheEvict(value = CacheKey.KEY_FINANCE, allEntries = true)
@Scheduled(cron = "${scheduler.scrap.yahoo}")
public void yahooFinanceScheduling() {

 

스프링 레디스 객체  : ReddisCacheManager

Annotation

  • @Cacheable: method의 결과값을 캐싱함. 동일한 argument로 method call이 이루어졌을때, 캐싱한 값을 return함.
  • @CachePut: method의 결과값을 캐싱함. 동일한 argument로 method call이 이루어진것과 관계없이 항상 캐싱하여 refresh함. 신규로 데이터가 들어오거나 update되는 메소드에 유용함.
  • @CacheEvict: method의 argument가 들어오면 해당 argument에 해당하는 캐쉬값을 삭제함.

일반 레디스 객체  : ReddisTemplate

 

 

Reddison을 사용한 분산락 도입

1. Gradle Implementation

의존성으로 reddisson을 추가한다. spring redis는 주로 캐쉬 목적으로 사용되기때문에 redisson은 포함되지 않는다.

implementation 'org.redisson:redisson:3.22.0'

 

2. Redisson Client Bean 등록

public class redisConfig {

    @Value("${spring.data.redis.port}")
    private int port;
    @Value("${spring.data.redis.host}")
    private String host;

    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://" + host + ":" + port);

        return Redisson.create(config);
    }
}

 

3. RedissonClient를 사용하여 Lock Service만들기

 

Redisson 주요 Lock API 종류

Redisson은 대신 Redis의 Pub/Sub 메커니즘을 활용하여 효율적인 락 대기 방식을 구현하고 있습니다. 이를 통해 Redis 서버에서 락이 해제되는 이벤트를 감지하고, 클라이언트가 락을 효율적으로 획득할 수 있도록 돕습니다.

 

  1. Reentrant Lock (재진입 가능한 락) - RLock : 재진입 가능한 락으로, 동일한 스레드가 여러 번 락을 획득해도 문제가 발생하지 않는 락입니다.
  2. Fair Lock (공정 락) - RFairLock :  공정 락은 락 요청이 순서대로 처리되도록 보장합니다. 락을 요청한 순서에 따라 차례로 락을 획득하게 되어, 먼저 락을 요청한 스레드가 먼저 락을 얻을 수 있습니다.
  3. Read/Write Lock (읽기/쓰기 락) - RReadWriteLock : 읽기/쓰기 락은 여러 스레드가 동시에 읽을 수는 있지만, 쓰기 작업이 수행되는 동안에는 읽기 작업이 차단되는 구조입니다. 읽기 락과 쓰기 락을 분리하여 데이터 일관성을 보장하면서 동시성을 최적화할 수 있습니다.
  4. Semaphore (세마포어) - RSemaphore : 세마포어는 특정 자원의 사용 가능 개수를 제한하는 동기화 도구입니다. 예를 들어, 데이터베이스 연결 개수와 같이 리소스의 최대 사용 가능 개수를 제어하는 데 유용합니다.
  5. CountDownLatch (카운트다운 래치) - RCountDownLatch : 카운트다운 래치는 여러 스레드가 특정 조건이 충족될 때까지 기다리도록 하는 동기화 도구입니다. 일반적으로 일정 수의 작업이 완료될 때까지 대기하는 경우에 사용됩니다.

이번 예시에서는 Reentrant Lock을 사용하여 구현한다.

 

Reentrant Lock  주요 메서드와 사용 예제

  • getLock() : getLock("lockName")을 호출하면 Redis 내에서 "lockName"을 식별자로 사용하여 락 객체를 생성하거나 기존의 락 객체를 참조하게 됩니다. 메서드는 Redisson에서 특정 이름의 락 객체를 가져오는 역할을 합니다.
  • lock.lock() : 락을 무기한으로 획득합니다. 락을 사용할 수 있을 때까지 현재 스레드는 대기하게 되며, 락을 획득하면 자동으로 해제되지 않습니다. 무한히 반복해서 락을 확인하는 방식(busy-waiting)이 아아니고 Redis는 락이 해제되면 기다리고 있는 클라이언트에게 알림을 보냄.
    • unlock() : 락을 해제합니다. 락을 획득한 스레드만 해제할 수 있으며, unlock()을 호출하지 않으면 락이 유지됩니다.
    • lock(long leaseTime, TimeUnit unit) : 락을 지정된 leaseTime 동안만 유지합니다. 지정된 시간이 지나면 자동으로 해제됩니다. 서버가 비정상 종료되는 경우에도 leaseTime 이후 자동으로 락이 해제되도록 보장합니다.
  • lock.tryLock() : 락을 즉시 시도하여 획득할 수 있으면 true, 그렇지 않으면 false를 반환합니다. 대기하지 않고 즉시 반환하므로, 락을 사용할 수 있는지 간단히 확인할 때 유용합니다. 비동기 환경에서 사용:
    • tryLock() 메서드는 락을 즉시 사용할 수 있는지 확인할 수 있어 비동기 프로세스에서 유용합니다. 
    • tryLock(long waitTime, long leaseTime, TimeUnit unit) : 지정된 waitTime 동안 락을 기다린 후, 락을 획득하면 leaseTime 동안 유지합니다. waitTime이 지나도록 락을 얻지 못하면 false를 반환하며, 락을 얻으면 true를 반환합니다.
  • isLocked() : 현재 락이 사용 중인지 확인합니다. 락이 획득되어 있으면 true, 그렇지 않으면 false를 반환합니다.

 


public void lock(long postId) {
    RLock lock = redisson.getLock(getLockName(postId));

    try{
        for (int trialTime = 0; trialTime < RETRY_COUNT; trialTime++) {
            log.info("Trying Lock for PostId : {} for {} try", postId, trialTime);
            boolean isLock = lock.tryLock(RETRY_TIME,RETENTION_TIME, TimeUnit.MILLISECONDS);
            if(isLock) {
                log.info("Lock success to get for PostId : {}", postId);
                return;
            }
        }
        log.info("Lock failed to get for PostId : {}", postId);
        throw new BizException(ParticipationErrorCode.PARTICIPATION_CONTENTION);
    }catch (Exception e){
        log.error("Redis Lock Exception", e);
        throw new BizException(ParticipationErrorCode.LOCK_ACQUISITION_ERROR);
    }
}

 

 

4. LockService를 사용하여 AOP를 만들기