쇼핑몰 재고관리 - 동시성 문제 Redis 분산락(Distributed lock)으로 해결하기

조성현·2023년 8월 16일
2

글을 시작하며

아래의 내용은 앞전 글에서 작성하였기에 링크만 따로 첨부합니다.
1. DB락과 분산락의 차이 [링크]
2. Lettuce가 아니라 Redisson Client를 선택한 이유 [링크]
3. Redis 분산락의 장단점 (MySQL 네임드락과 비교) [링크]


1. 분산락을 구현해보자

현재 주문API는 장바구니에서 여러 상품 구매, 상품 상세페이지에서 바로 구매를 전제로 1~n개 종류의 상품 주문을 받을 수 있도록 설계되어있습니다.

  • 이에 따라 Lock도 상품별로 획득하도록 Lock name을
    productId: {productId}로 설정해주었습니다.

분산락 로직 설명 (2번째 사진)

  • line22~25
    -> productIds를 stream으로 돌리며 List<RLock>을 생성
  • line 27~31
    -> for문을 돌며 분산락 획득 시도 대기시간 10초, 락 유효시간 1초로 설정
  • line 32
    -> 비즈니스 로직 호출
  • line 35~36
    -> 분산락 반납.

line 35~36을 보면 try catch 이후의 finally문에서 분산락을 반납하고 있습니다
-> 트랜잭션 커밋이후에 분산락을 반납(해제)해야만 하는 이유가 있기 때문인데요... 그 이유는 무엇일까요?


2. 트랜잭션 커밋 후에 락을 해제해야하는 이유

위 서비스 로직처럼 트랜잭션 내부에서 분산락 반납을 한다면?

사진출처 - 마켓컬리 기술블로그[링크]

  • 트랜잭션 커밋 이전에 락을 반납함으로써, 다른 트랜잭션이 커밋 이전 시점의 재고 조회를 해오게 되고,
    갱신손실(Lost Update)이 발생하여 데이터정합성이 깨지게 되는 문제가 발생합니다.
  • 그 외에도 최악 of 최악의 상황이라면 데드락도 발생할 수 있습니다.

이렇듯 여러 문제들이 발생할 수 있으므로, 트랜잭션 범위 밖에서 분산락을 반납해주는 것이 필요합니다.

반납은 그렇다 치고 획득은 왜 트랜잭션 밖에서 하나요?

제가 개인적으로 생각해본 이유 입니다.

  1. 트랜잭션이 시작되기 위해서는 DB의 커넥션을 가져와야 합니다. 트랜잭션이 진행되는 동안 해당 커넥션을 트랜잭션이 소유하게 되는데요.
    이를 커넥션을 물고 있다라고 표현하기도 합니다.
  • 커넥션 풀은 한정적인데, 분산락을 획득하지도 않은 트랜잭션들이 커넥션을 물고 분산락 획득을 대기한다? -> 커넥션이 마르거나, 다른 API 호출들이 커넥션을 획득하지 못하는 불상사가 생길 수 있습니다.
  • 이러한 문제를 막기 위해 분산락을 먼저 획득하고, 트랜잭션이 시작될 수 있도록 해주는게 아닌가~ 라고 생각합니다.

3. 현재 버전의 성과와 한계점

3-1 무엇을 해냈는가

여러 주문이 동시에 들어오는 동시성 문제를 productId를 키값으로 하는 분산락을 구현함으로써 해결하였다.

  • 또한 1~n개의 상품 종류가 담긴 주문의 동시성문제를 해결했다는 점에서, 장바구니 구매상품 상세페이지 바로구매를 하나의 API로 처리할 수 있도록 했다는 의의가 있다.

  • 단, 여러개의 상품종류를 주문할 경우 상품 번호 순으로 오름차순 정렬해서 분산락을 획득하도록 해야만, 데드락을 예방할 수 있다.

3-2. 어떤 한계점이 있는가

단시간에 많은 요청이 폭주하는 상황을 전제로 했을 때!

IF 상품1, 2, 3을 한번에 주문하는 경우

  • productId:1, productId:2,productId:3 의 분산락을 모두 획득해야합니다.
  • 3개의 분산락을 모두 획득해야만 비즈니스 로직을 시작할 수 있는데, 만약 상품1의 분산락을 먼저 획득하고 상품2,3을 기다리게 된다면 어떻게 될까요?
    -> 현재 기준 분산락 유효시간은 1초이기 때문에 예외가 발생하며 끝나게 됩니다.
  • 상품 1의 분산락을 점유하고 있었기에 해당 상품을 구매하고자 했던 다른 요청들은 꼼짝없이 기다릴 수 밖에 없습니다..

그렇다면 분산락 유효시간을 길~게 해주면 해결 될까요?

분산락 유효시간을 길게 해준다 라는 대응은,
상품 1,2,3을 한번에 주문하는 케이스처럼 분산락 획득을 모두 획득하는 케이스들이 주구장창 분산락을 물고서 '다른 상품 분산락'을 기다리는 문제를 발생시키게 될 우려가 있습니다.


4. 트래픽 스파이크에 대응하려면?

현재 프로젝트도 상품번호 순으로 정렬만 되어있다면, 소소한 트래픽 정도는 감당할 수 있습니다.
그러나 선착순 이벤트와 같이 단기간에 많은 요청이 몰리는 상황에서는 분산락 획득 요청 타임아웃이 속출하면서 백엔드 개발자의 휴대전화에 불이 나게 될 것입니다.(시스템 장애 콜)

  • '트래픽 스파이크'에 어떻게 대응할 수 있을까요?

4-1. 현재 코드를 최대한 유지하는 방법

행사상품은 단품으로만 구매할 수 있도록 한다.

  • 일단 행사상품 상세페이지에서 장바구니 담기를 없애고,
  • 백엔드에서도 행사상품들이 장바구니 결제로 다른 상품들과 결제되는지 체크하고 막는다.
  • 위 방법을 통해 트래픽이 몰리는 행사상품 구매요청에서는 여러 상품의 분산락을 획득하기 위해 낭비되는 시간(리소스)을 줄일 수 있음과 동시에
  • 행사상품이 아닌 상품들은 기존과 동일하게 1~n개의 상품종류를 한번에 주문할 수 있다.
    .

4-2. Redis로 재고를 관리한다.

관련 Ref
1. 배민 - 선물하기 시스템의 상품 재고는 어떻게 관리되어질까? [링크]
2. 여기어때 - Redis&Kafka를 활용한 선착순 쿠폰 이벤트 개발기 (feat. 네고왕) [링크]

  • 위 링크들에서는 Redis Transaction으로 재고 조회와 차감을 묶고, Set 자료구조를 사용하여 Redis로 재고를 관리하고 있다.

  • 평시에 적용할만한 방법이라기 보다는, 선착순 이벤트와 같이 예측가능하면서도 단시간에 폭발적인 트래픽 이슈에 대한 대응방안으로서 좋은 것 같다고 생각한다.
    (재고 히스토리를 RDB에 기록하긴 하지만, 재고에 대한 관리자체는 Redis에서 이루어지고 RDB에 남은 재고수치를 기록하거나 하진 않는다.)

위 방식의 장점은 무엇일까?

  1. 재고 조회와 차감에 대해서만 Redis Transaction 기반으로 동기적으로 작동하기에 (모든 비즈니스로직을 동기적으로 처리하는) -> 분산락 방식보다 더 빠르다.
  2. Redis의 SET 자료구조를 사용함으로써, 재고사용량 차감에 대한 잘못된 구매번호의 이벤트가 발행되어도 재고사용량 차감에 영향을 미치지 않는다.

단점

  1. 재고사용량을 Redis에서 관리하고 있기에, 레디스가 갑자기 다운된다면
    재고가 얼마나 남았는지 파악하는데 많은 리소스가 필요하다.
    (재고 히스토리를 토대로 계산을 하던가 해야됨)

5. 추후 도전과제 (AOP)

참고 Ref
풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson [링크]

AOP로 분산락 구현시 장점

  1. 분산락 처리 로직은 비즈니스 로직이 오염되지 않게 분리해서 사용한다.
  2. waitTime, leaseTime을 커스텀 하게 지정 가능하다.
  3. 락의 name에 대해 사용자로부터 커스텀 하게 받아 처리한다.
  4. 추가 요구사항에 대해서 공통으로 관리한다.

6. 그 외에 알게된 점.

6-1 Redis Expired Event는 사실 만료시점에 발생하지 않는다.

Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero.

Redis의 pub/sub를 이용한 재고관리 시스템을 구상해보다가 위 사실을 알게되었다.

  1. productId를 키로 레디스에서 재고 조회시 없다면 RDB 조회해서 Redis 세팅하고 재고차감, 있다면 그냥 Redis 재고차감 하는 식으로 재고를 관리하고
  2. 해당 키가 만료될 때, 키 만료 이벤트를 발행하여 재고를 RDB에 반영

위 시스템을 구상했으나, expired event의 발행시점이 너무 늦어져서, 재고 데이터 정합성이 틀어질 확률이 매우 높기에 해당 아이디어를 기각하게 되었다.

profile
맛있는 음식과 여행을 좋아하는 당당한 뚱땡이

2개의 댓글

comment-user-thumbnail
2023년 8월 16일

유익한 글이었습니다.

1개의 답글