본문 바로가기
공부

Redisson Client, Embedded Redis 기본 설정 및 테스트

by son_i 2024. 3. 19.
728x90

스프링 부트 스타터 레디스에는 Lettuce 라이브러리를 사용 (Spring-Boot-Starter-Redis)

Lettuce는 Netty 기반의 비동기 방식으로 Jedis보다 사용이 권장됨.

그러나 스핀락 방식을 활용하고 있기 때문에 락 만료시간에 대한 정책이 필요하다.(순회 횟수 제한, 시간으로 제한 etc...)

 

스핀락 방식 : 계속해서 Lock을 획득하기 위해 순회하는 방식. 만약 lock을 획득한 스레드나 프로세스가 lock을 정상적으로 해제해주지 못 한다면 다른 스레드에서는 계속 lock을 획득하기 위해 시도하느라 실행되는 애플리케이션에 장애가 될 수 있음.
=> 이를 위해 락 만료시간에 대한 정책이 필요

+ 또한 Lock을 획득하기 위해 순회하는 동안 계속해서 레디스에 요청을 보내게 되는데 이런 요청을 보내는 스레드가 많으면 레디스에 부하가 발생할 수 있다.

 

- Redisson이란 ?)

   스핀락 방식의 문제를 해결하기 위한 분산락 방식을 도입한 Lettuc와 비슷한 자바 레디스 클라이언트.

 

   Lettuce와 비슷하게 Netty를 사용해서 비동기 논블로킹 I/O를 제공.

 

   특이하게 레디스의 명령어를 직접 제공하지 않고 Lock과 같은 특정한 구현체의 형태를 제공한다.

 

 

- Redisson 특징)

  pub-sub 기반으로 Lock 구현 제공

 

  pub-sub 방식이란 채널을 하나 만들고 락을 점유 중인 스레드가 락을 해제했음을 대기 중인 스레드에게 알려주면 대기 중인 스레드가 락 점유를 시도하는 방식.

 

   이 방식은 Lettue와 다르게 대부분 별도의 Retry 방식을 작성하지 않아도 된다.

 

- 프로젝트 적용)

1. 의존성 추가

// https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter
implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.27.2'

 

나는 스프링부트 3.1.3 버전을 사용하고 있으므로 redisson-spring-data-3x 버전을 사용해야한다.

https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter

참고.

 

+ 또 Spring boot 2.x에서 3.x에서 변경될 때 properties 설정도 변경된다.

Spring Boot 2.x

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=16379
spring.redis.password=mypass
spring.redis.timeout=60000


Spring Boot 3.x

spring.data.redis.database=0
spring.data.redis.host=localhost
spring.data.redis.port=16379
spring.data.redis.password=mypass
spring.data.redis.timeout=60000

 

2. LocalRedis 실행 설정

  • Spring Boot가 기동하면서 Bean을 등록할 때 레디스를 실행하고, 종료되면서 Bean을 삭제할 때 레디스를 종료하도록 설정
  • 주의점 : 해당 Bean이 Redis Repository보다 빨리 뜰 수 있도록 패키지 순서를 위쪽으로 해야한다.
@Configuration
public class LocalRedisConfig {
    @Value("${spring.data.redis.port}")
    private int redisPort;

    private RedisServer redisServer;

    @PostConstruct
    public void startRedis() { //빈을 생성 하자마자 실행
        redisServer = new RedisServer(redisPort);
        redisServer.start(); //redis하나를 별도의 인스턴스로 띄워줌
    }

    @PreDestroy
    public void stopRedis() { //빈 파괴 직전에 실행
        if (redisServer != null) {
            redisServer.stop();
        }
    }
}

 

3. RedisRepository 등록 → redis 접근 위한 redisson client를 bean으로 등록.

@Configuration
public class RedisRepositoryConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;

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

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);

        return Redisson.create(config);
    }
}

 

4. Controller, RedisTestService 작성

Redis를 테스트하기 위해 컨트롤러와 서비스 코드를 작성했다.

 

<RedisTestController>

@RestController
@RequiredArgsConstructor
public class RedisTestController {
    private final RedisTestService redisTestService;

    @GetMapping("/get-lock")
    public String getLock() {
        return redisTestService.getLock();
    }

 

 

<RedisTestService>

@Slf4j
@Service
@RequiredArgsConstructor
public class RedisTestService {
    private final RedissonClient redissonClient;
    //RedisRepositoryConfig에서 만들어둔 redissonClient bean과 이름이 같으므로 자동 주입이 됨.

    public String getLock() {
        RLock lock = redissonClient.getLock("sampleLock");
        //1. redissonClient에 sampleLock을 키로 가져오고

        try { //2. spinLock 시도 1초동안 lock을 시도하고 3초동안 lock을 가지고 있음.
            boolean isLock = lock.tryLock(1, 5, TimeUnit.SECONDS);
            if (!isLock) { //lock 취둑 실패
                log.error("====== Lock acquisition failed =====");
                return "Lock failed";
            }
        } catch (Exception e) {
            log.error("Redis lock failed");
        }
        //명시적으로 unlock을 하고 있지 않기 때문에
        //다른 녀석이 lock취득하려고 하면 5초동안은 실패하게 될 것임
        return "Lock success";
    }
}

 

 

* Trouble Shooting

 (Spring boot 3점대는 spring.data.redis.xxx 사용해야함)

1. spring.redis.port 실행시키니까 아래와 같이 redisRepositoryConfig를 만들 수 없는데 String으로 입력된 port값을 int로 바꿀 수 없다는 것이었다. 강의에서는 int형으로 했지만 RedisRepositoryConfig에 String으로 바꿔줘본다.

2024-03-19T20:05:33.410+09:00  INFO 8816 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-03-19T20:05:33.622+09:00  WARN 8816 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'redisRepositoryConfig': Unsatisfied dependency expressed through field 'redisPort': Failed to convert value of type 'java.lang.String' to required type 'int'; For input string: "{$spring.data.redis.port}"
2024-03-19T20:05:33.623+09:00  INFO 8816 --- [           main] redis.embedded.AbstractRedisInstance     : Stopping redis server...
2024-03-19T20:05:33.629+09:00  INFO 8816 --- [           main] redis.embedded.AbstractRedisInstance     : Redis exited
2024-03-19T20:05:33.629+09:00  INFO 8816 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'

-> ㅋㅋ 문제는 해결되고 또다른 문제가 났었는데 이제보니까 그냥 오타 때문이었다.

ㅡㅡ "${spring.data.redis.port}"로 바꾸니 모든 문제 해결..

 

음 ? 근데 spring.data.redis.port가 아닌 spring.redis.port로 해줘도 정상적으로 동작한다 뭐징? ㅋㅋ.....