본문 바로가기
CS/DB

동시성 이슈를 해결하기 위한 Redisson 적용기

by 봄의 개발자 2024. 5. 5.

들어가며

현재 개인프로젝트로 예약 구매 이커머스 프로젝트를 진행하고 있다. 

출처: 항해 취업 리부트 코스

특정 시간에 구매 버튼이 활성화되는 예약 구매 시스템이다. 해당 시간에는 많은 트래픽이 몰리는 상황을 조건으로 걸 예정이다. 

이를 구현하기 위해 동시성 이슈를 학습하고 이를 해결하는 방법에 대해 알아보았다. 

다양한 방법이 존재했지만 그중에서도 Redis의 Redisson을 사용하기로 했다. 

그래서 여러가지 방법과 그중에서도 Redisson을 선택한 이유에 대해서 이야기 해보려고 한다.

 

동시성 이슈란?

쉽게 말하면 공유된 자원에 동시에 접근해 발생하는 문제를 말한다.

결국 Race Condition(경쟁 상태)가 발생하게 되는 것인다.

 

Race Codition 란?

둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다.

 

 


 

동시성 이슈를 해결하는 방법

1. MySQL을 이용한 방법

Pessimistic Lock(비관적 락)

충돌이 발생할 확률이 높다고 가정해 실제로 데이터에 접근하기 전에 먼저 락을 걸고 충돌을 예방하는 방식이다.

  • 충돌이 많이 일어난다면 optimistic lock보다 성능이 좋을 수 있다.
  • 락을 통해 업데이트를 제어하기 때문에 데이터 정합성이 보장된다.
  • 별도의 락을 잡기 때문에 성능 감소가 있을 수 있다.

Optimistic Lock(낙관적 락)

충돌이 거의 발생하지 않을 것이라 가정해 충돌이 발생한 경우에 대응하는 방법이다.

  • 별도의 락을 잡지 않아서 pesssimistic Lock보다 성능상 이점이 있다.
  • 업데이트 실패했을 때 재시도 로직을 개발자가 직접 작성해 주어야 한다.
  • 충돌이 빈번하게 일어나지 않을 것으로 예상되는 경우에 사용한다.

Named Lock

MySQL에서 제공하는 락으로 MySQL서버의 메모리 내에서 공유 리소스에 대한 접근을 직접 동기화할 수 있다.

  • 실제 데이터가 아닌 별도의 공간에 lock을 건다.
  • 같은 데이터 소스를 사용하면 커넥션 풀이 부족해지는 현상으로 인해 다른 서비스에도 영향을 줄 수 있으므로 데이터 소스를 분리해서 사용하는 것이 좋다.

 

2. Redis를 이용한 방법

Lettuce

Lettuce는 setnx 명령어를 활용하여 분산락을 구현한다.

setnx: key와 value를 set할 때 값이 없을 때만 set하는 명령어

스핀락을 사용해서 재시도 로직을 직접 구현해야 한다.

  • 구현이 간단하다.
  • spring-data-redis 를 이용하면 lettuce 가 기본이기때문에 별도의 라이브러리를 사용하지 않아도 된다.
  • spin lock 방식이기때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis 에 부하가 갈 수 있다.
    → 스레드 슬립을 통해 락 획득 재시도 간에 텀을 둬야한다.

Redisson

Redisson은 pub-sub 기반의 lock 구현을 제공한다.

pub-sub 방식: 채널을 하나 만들어 lock을 점유하고 있는 쓰레드가 다음 차례의 쓰레드에게 점유가 끝났음을 알려주면서 lock을 주고 받는 방식이다.

  • 락 획득 재시도를 기본으로 제공한다.
  • pub-sub 방식으로 구현이 되어있기 때문에 lettuce 와 비교했을 때 redis 에 부하가 덜 간다.
  • 별도의 라이브러리를 사용해야한다.

Redisson 선택 이유

이미 프로젝트에서 Redis를 사용하고 있기도 했고, 인메모리 db인 Redis가 더 빠른 속도를 가지므로 Redis를 사용하는 건 확정되었다. 그러나 Lettuce와 Redisson 중에 뭘 선택해야할지 고민이 컸다. 처음에는 Lettuce를 사용하려 했었다. spring-data-redis를 사용하면 기본적으로 Lettuce가 기본적으로 포함되어 있어서 별도의 라이브러리를 사용하지 않아도 된다는 점 때문이었다. MSA로 서비스를 구현하다보니 생각보다 의존하고 있는 라이브러리가 많아서 이제는 라이브러리 사용을 줄이고 싶은 마음에 그렇게 생각했었다. 그렇지만 Lettuce가 스핀락 방식이라 많은 스레드가 동시에 lock 획들을 위해 대기하고 있다면 redis에 부하가 갈 수도 있다는 것이 좀 큰 단점이었다. (스핀락은 락을 얻을 때까지 계속 루프를 돌면서 시도함)

결국 pub-sub방식을 사용해 비교적 부하가 적은 Redisson을 선택하기로 했다. 

 


Redisson 락 예시

@Transactional
public void decrease(Product product, OrderProductUpdateReqDto orderProductUpdateReqDto) {
    RLock lock = redissonClient.getLock(String.valueOf(product.getId()));
    try {
        boolean isLocked = lock.tryLock(5, 1, TimeUnit.SECONDS);

        if (!isLocked) {
            log.debug("상품 재고 감소 LOCK 획득 실패");
            return;
        }

        product.updateQuantity(orderProductUpdateReqDto.getQuantity());

    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        lock.unlock();
    }
}