동시성 문제 - Lettuce 사용

김건우·2023년 1월 23일
1
post-thumbnail

동시성 문제에서 Redis의 Lettuce 사용한 이유

  1. DB를 이용한 동시성 해결방법
    DB를 이용하여 분산락을 구현해도 되지만, 락을 잡기위해 간단한 락 정보를 저장하는 테이블을 만들어야 하는게 좋은 선택인지 의문이 들었습니다.
    또한, 락 정보는 영구적인 데이터가 아닌 휘발성 데이터에 더 가깝다고 생각하여 DB를 이용하는 것보다 Redis를 이용하는 것이 성능으로 최적화 될 것이라고 생각했습니다.
  2. Redis 이용
    Redis가 Single Thread 기반이기 때문에 동시성 제어할 때 좋은 선택지라고 생각했습니다.
    락 정보가 간단한 휘발성 데이터에 가깝다고 생각했기 때문에 Redis를 이용하기로 결정

Lettuce

  • Setnx 명령어를 활용하여 분산락을 구현 (Set if not Exist - key:value를 Set 할 떄. 기존의 값이 없을 때만 Set 하는 명령어)
  • Setnx 는 Spin Lock방식이므로 retry 로직을 개발자가 작성해 주어야합니다.
  • Spin Lock 이란, Lock 을 획득하려는 스레드가 Lock을 획득할 수 있는지 확인하면서 반복적으로 시도하는 방법입니다.

여기서 잠깐, SETNX란?
SET if Not eXist의 줄임말로, 특정 key 값이 존재하지 않을 경우에 set 하라는 명령어 입니다. 특정 키에 대해 SETNX 명령어를 사용하여 value가 없을 때만 값을 세팅하는, 즉 락을 획득하는 효과를 낼 수 있습니다.

Spin Lock 과정

Redis 환경 설정
1. redis 이미지 다운로드 : $ docker pull redis
2. redis 실행 : $ docker run --name myredis -d -p 6379:6379 redis
3. 실행 확인 명령어 : $ docker ps

  • Docker에 redis 추가🔽

Spring Redis 의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //redis 의존성 추가
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

setnx 명령어 사용하기

  1. 먼저 컨테이너 id 를 사용하여 cli 로 접속합니다.
    • docker exec -it <도커 이미지> redis-cli
  2. setnx 명령어는 key 와 value 로 삽입합니다.
    a. setnx (key) (value)
    b. 1 이라는 key 로 맨처음 삽입할때는 성공하지만, 그 이후로는 실패합니다.
    c. del (key) 명렁어로 해당 key의 데이터를 삭제하고 다시 삽입하면 성공합니다
  • setnx를 사용한 key value 값 set🔽

Spring 에서 Redis Lettuce 적용

RedisLockRepository

key를 이용한 Lock과 unLock 메소드 정의

@Component
@RequiredArgsConstructor
public class RedisLockRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public Boolean lock(final Long key) {
        return redisTemplate
            .opsForValue()
            //setnx 명령어 사용 - key(key) value("lock")
            .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    }

    public Boolean unlock(final Long key) {
        return redisTemplate.delete(generateKey(key));
    }

    private String generateKey(final Long key) {
        return key.toString();
    }
}
  1. SpinLock 방식으로 락을 얻기를 시도하고,
  2. 락을 얻은 후, 재고 감소 비지니스 로직을 처리합니다.
  3. 그 후, 락을 해제해주는 방식이 Lettuce 방식입니다.
  • 퍼사드 패턴을 사용하여 redis의 repository와 stockService를 주입해줍니다.🔽
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade  {

    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;

    public void decrease(final Long key, final Long quantity) throws InterruptedException {
        // Lock 획득 시도
        while (!redisLockRepository.lock(key)) {
            //SpinLock 방식이 redis 에게 주는 부하를 줄여주기위한 sleep
            Thread.sleep(100);
        }

        //lock 획득 성공시
        try{
            stockService.decrease(key,quantity);
        }finally {
            //락 해제
            redisLockRepository.unlock(key);
        }
    }
}

Thread.sleep(100);의 중요성
Sprin Lock 방식이, Lock 을 얻을 떄까지 Lock 얻기를 시도하기 떄문에, 계속해서 Redis 에 접근해서 Redis에 부하를 줄 수 있다는 단점이 존재합니다. 그래서 Thread.sleep(100)를 사용하여 redis에 갈 수 있는 부하를 줄여줍니다.

Lettuce의 동시성 해결 테스트코드

다음은 테스트 코드입니다. 본인의 테스트 환경에서는 49.581초 소요되었습니다🔽

// StockServiceTest.java
@DisplayName("redis lettuce lock 을 사용한 재고 감소")
    // Redis를 사용하면 트랜잭션에 따라 대응되는 현재 트랜잭션 풀 세션 관리를 하지 않아도 되므로 구현이 편리하다.
    // Spin Lock 방식이므로 부하를 줄 수 있어서 thread busy waiting을 통하여 요청 간의 시간을 주어야 한다.
    @Test
    void LETTUCE_LOCK을_사용한_재고_감소() throws InterruptedException {
        // given
        // when
        IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> {
                    try {
                        lettuceLockStockFacade.decrease(productId, quantity);
                    } catch (InterruptedException ex) {
                        throw new RuntimeException(ex);
                    } finally {
                        countDownLatch.countDown();
                    }
                }
        ));
countDownLatch.await();
        // then
        final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity();
        System.out.println("### LETTUCE LOCK 동시성 처리 이후 수량 ###" + afterQuantity);
        assertThat(afterQuantity).isZero();
    }

Lettuce의 특징

  1. 구현이 간단하다
  2. Spring data redis를 이용하면 lettuce가 기본이기 떄문에 별도의 라이브러리를 사용하지 않아도 된다.
  3. Spin Lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있다.
profile
Live the moment for the moment.

0개의 댓글