재고 처리 방법(방식 및 동시성 제어) - ekdan38/HotDealService GitHub Wiki
1. 상품 재고 구조
기본적으로 상품의 원본 재고 수량은 Product 테이블에 저장
하지만 단순히 RDB만 사용하여 재고 관련 처리를 하면 DB 병목이 발생하기 때문에, Redis를 활용한 실시간 재고 캐싱을 사용했음
2. 재고 점유 방식 사용
단순하게 Redis 재고만으로 재고 관리를 한다면, 다음과 같은 문제가 발생할 수 있음
- 재고 감소 시점 : Redis로 재고 감소를 처리한다면, 주문 시점에 처리하는게 결제 결과보다 합리적. 하지만 재고 감소 시점은 결제 결과에 따른 처리가 옳음
- 트랜잭션 일관성 : Redis 와 DB 트랜잭션과 함께 처리 불가
- 장애 시 데이터 유실 : Redis는 메모리 기반이기 때문에, 문제가 생겼을때 재고 유실 가능성
따라서 다음과 같은 설계로 재고 관리를 처리했음
- 재고만 Redis 캐싱 처리
- 재고 점유 테이블로 재고 점유를 처리하고, Redis 재고와 비교하여 점유가 가능하다면 Status를 기록하고 Status 기반으로 점유를 기록
[주문 요청 흐름]
- 재고 수량 Redis에서 확인
- 점유 가능한 경우, DB에 StockReservation INSERT (status = RESERVED)
- 결제 완료 시, status = CONFIRMED + Redis 재고 감소
- 결제 실패 시, status = CANCELED
점유 테이블을 사용하는 설계는 다음과 같은 이점이 존재
- 재고 적합성 보장 : status 기반으로 재고 점유 상태 관리 가능
- 결제 결과에 따른 재고 복구 용이 : RESERVED -> CANCELED
- 트랜잭션 일관성 : 재고 점유는 DB 트랜잭션 내부에서 안전하게 처리
- 장애 복구 가능 : 재고 점유에 대한 내역이 DB에 남아있어 추적 가능
- 사용자 경험 : 사용자는 주문시에 재고에 대한 점유가 확실하여 결제 처리 도중 재고에 대해 소유 보장
3. 재고 점유 처리 동시성 문제
이 구조에서 재고 점유 처리 시, Redis + 재고 점유 테이블에 동시에 접근해야 함
여러 사용자가 동시에 주문 요청을 보내면 다음과 같은 동시성 문제 발생 가능
- 재고 수량이 충분하다고 판단했지만, 점유 과정에서 다른 트랜젹션이 먼저 점유
- 오버 부킹, 동시성 충돌, 레이스 컨디션 발생 가능
다음과 같은 동시성 제어 전략을 고민함
- synchronized
- 구현이 간단
- 단일 인스턴스에서만 동작 -> MSA 환경에서 부적합
- 비관적 락
- 동시성 제어 가능, 하지만 DB 부담이 커짐
- 낙관적 락
- 동시성 제어 가능
- 비관적 락보다 성능 우수
- 재시도 로직 필요
- Redis(Lettuce)
- 동시성 제어 가능
- 구현 간단
- 스핀락 방식(반복적으로 락 획득 시도)
- 많은 스레드가 락을 획득한 상태라면 부하 가능성
- Redis(Redisson)
- 동시성 제어 가능
- pub/sub 방식(락 해제 신호 수신)
- 락 획득 재시도 기본 제공
- 동일 서비스의 인스턴스 간 동기화 가능 -> MSA 환경에 적합
이와 같은 이유로 Redis(Redisson) 분산락을 적용 하였음