-
Notifications
You must be signed in to change notification settings - Fork 2
(레거시) 주문 처리로 인한 재고 차감에서 발생하는 동시성 이슈 문제를 어떻게 해결할까?
daadaadaah edited this page Sep 20, 2024
·
1 revision
- 주문 처리로 인한 재고 차감에서 발생하는 동시성 이슈 문제를 어떻게 해결할까?
1. 딜 상품 조회 및 검증
2. 딜 상품 재고 감소
3. 딜 상품 정보 업데이트
/*
* 1. [A 트랜잭션] 딜 상품 조회 : 2개
* 2. [B 트랜잭션] 딜 상품 조회 : 2개
* 3. [A 트랜잭션] 주문량(2개) 만큼 재고 감소
* : 2(조회 결과) - 2(주문 수량) = 0
* 4. [B 트랜잭션] 주문량(1개) 재고 감소
* : 2(조회 결과) - 1(주문 수량) = 1
* 5. [A 트랜잭션] 재고량 0으로 딜 상품 업데이트
* 6. [B 트랜잭션] 재고량 1로 딜 상품 업데이트 // 🧨 데이터 일관성 깨짐, 실제 재고량이 0으로 품절인데, 1로 업데이트 되는 문제 발생
* /
- 다음과 같은 생각의 흐름으로
딜 상품과 재고 분리하는 구조
와Redis DECR 활용해서 재고 사후 검증 후 rollback 시키는 방법
으로 동시성 이슈 문제를 해결했습니다.
- 맨 처음에는 동시성 이슈 문제를 해결할 때 가장 흔하게 사용하는
분산락
을 사용해보자라는 생각을 하였습니다. - 그런데, 분산락을 사용할 경우, 작업을 복잡하게 만들고, 디버깅하기 어려운 문제가 있습니다.
- 멀티쓰레드 환경에서 발생하는 버그들이 재현하기도 함들고, 특수한 케이스에서만 발생되기 때문에, 오랜 시간이 지나서야 결함이 발견되는 문제가 있습니다.
- 또한, 설정한 락 임대하는 시간보다 실제로 더 걸려서, 로직이 다 수행하기도 전에 락이 해제되고 다른 쓰레드가 락을 잡게 될 경우, 개발자가 의도하지 않은 대로 동작할 수 있는 문제가 있습니다.
- 위 같은 분산락의 문제점 때문에, 가능한 락 로직을 배제하는 방법이 없을까에 대해 고민하면서 왜 락이 필요한지에 대해 생각해봤습니다.
- 분산락이 필요한 이유는 아래 3가지 로직이 원자성 있게 처리되어야 되기 때문이었습니다.
1. 딜 상품 조회
2. 딜 상품 재고 감소
3. 딜 상품 정보 업데이트
- 그러면, 위 3가지 로직을 단순화 시키는 방법이 없을까? 라는 생각한 결과, 2번 로직과 3번 로직을 한번에 처리해 주는 Redis의 DECR 를 사용해볼까? 라는 생각을 했습니다.
- 그런데, Redis의 DECR를 사용하게 되면, 딜 상품에서 재고를 분리시켜야 했습니다.
- 그래서, 다음과 같이 표를 만들어, 딜 상품과 재고를 통합했을 때와 분리했을 때의 각각의 장단점에 대해 생각해봤습니다.
구조 | (A 구조) 딜 상품과 재고 통합형 저장 구조 | (B 구조) 딜 상품과 재고 분리형 저장 구조 |
---|---|---|
장점 | 1. CRUD 상황에서 ‘CRD 상황’(예: 상품 등록, 상품 조회, 상품 삭제 API)에서 B 구조보다 유리하다. (1) 딜 상품과 재고량이 이미 통합되어 있으므로, 개발자가 직접 원자성 있게 트랜잭션 처리를 고려하지 않아도 되므로, 애플리케이션 로직이 간단하다. (2) Redis에 1번만 Access하면 되므로, B 구조에 비해 읽기 또는 쓰기 부하가 덜하고, CRD 상황에서 B 구조보다 높은 TPS를 가질 수 있다. |
1. CRUD 상황에서 ‘U 상황’(예: 주문 API)에서 A 구조보다 유리하다. (1) 동시성 제어 로직이 필요없으므로, 애플리케이션 로직의 복잡도가 A 구조에 비해 간단하고, U 상황에서 A 구조보다 높은 TPS를 가질 수 있다. 2. 추후에 재고 도메인이 확장되어 분리해야 되는 상황이 발생할 때 A 구조보다 작업량이 줄어든다. |
단점 | 1. CRUD 상황에서 U 상황(예: 주문 API)에서 B 구조보다 불리하다. (1) 동시성 제어 로직이 필요하므로, 애플리케이션 로직의 복잡도가 증가한다. (2) 동시성 제어 로직으로 인해 U 상황에서 B 구조보다 낮은 TPS를 가질 수 있다. (3) 경우에 따라서 Redis 로직이 필요하므로, B 구조보다 높은 Redis 쓰기 부하가 발생할 수 있다. 2. 추후에 재고 도메인이 확장되어 분리해야 되는 상황이 발생할 때 B 구조보다 작업량이 많아지고, 그로 인한 실수도 발생할 수 있다. |
1. CRUD 상황에서 ‘CRD 상황’(예: 상품 등록, 상품 조회, 상품 삭제 API)에서 A 구조보다 불리하다. (1) 딜 상품과 재고량을 개발자가 직접 원자성 있게 트랜잭션 처리를 고려해야 하므로, 애플리케이션 로직의 복잡도가 증가한다. (2) 트랜잭션 실패로 인해 재시도 로직이 필요하고, 그로 인해 A 구조에 비해 읽기 부하가 발생할 수 있다. (3) Redis에 2번씩 Access해야 하므로, A 구조에 비해 읽기 또는 쓰기 부하가 더 높을 수 있다. (4) CRD 상황에서 B 구조보다 낮은 TPS를 가질 수 있다. |
- 그렇게 장단점을 비교해본 결과, CRD 상황에서 트랜잭션 처리로 복잡한 로직을 취하지만,
U 상황
에서 분산락 없는 간단한 로직을 취할 수 있는딜 상품과 재고 "분리형" 저장 구조
를 선택했습니다. - 왜냐하면, 현재 저희 서버 시나리오의 경우, U 상황은 트래픽이 몰리는 시나리오고, CRD 상황은 비교적 서버가 여유로운 시나리오입니다.
- 따라서, 트랙픽이 몰리는 U 상황에서 복잡한 로직을 취하는
딜 상품과 재고 "통합형" 저장 구조
보다 비교적 서버가 여유로운 CRD 상황에서 복잡한 로직을 취하는딜 상품과 재고 "통합형" 저장 구조
이 더 낫다고 생각했습니다. - 왜냐하면, 트래픽이 몰리는 U 상황에서 문제가 발생하면, 개발자가 대응하기 어렵고, 또한, 이때 문제가 발생하면 돈과 관련있는 로직이라서 비즈니스적으로도 리스크가 있다고 생각했습니다.
- 따라서, 서버가 비교적 여유로울 때 CRD 상황에서 문제가 발생하더라도 개발자가 대응하고 쉽고, 문제가 발생하더라도 비즈니스적으로 문제가 되는 포인트가 덜한
딜 상품과 재고 "분리형" 저장 구조
를 선택했습니다. - 그 결과, 로직은 다음과 같이 단순해졌습니다.
1. 재고 조회 및 검증
2. 재고 감소
- 그런데, 여전히 분산락을 통해 묶어주지 않으면, 다음과 같은 상황에서 데이터의 일관성이 안맞는 문제가 발생하는 상황이었습니다.
/*
* 1. [A 트랜잭션] 재고 조회 : 2개
* 2. [B 트랜잭션] 재고 조회 : 2개
* 3. [A 트랜잭션] 주문량(2개) 만큼 재고 감소 : 현재 재고량(2) -> 차감 후 재고량(0)
* 4. [B 트랜잭션] 주문량(1개) 재고 감소 : 현재 재고량(0) -> 차감 후 재고량(-1) 개 // 🧨 차감이 되지 않아야 하는데, 차감 됨
* /
- 이때, 재고 데이터의 일관성을 보장하기 위해 분산락을 사용하는 건데, 분산락을 사용하지 않으면서 일관성 보장하는 방법은 없을까? 라는 생각을 하였습니다.
- Redis의 DECR 명령어 실행 후 return 값을 활용하여
재고를 사후 검증하는 방법
으로 하고, 잘못 감소가 이루어진 경우,Rollback
해준다면, 최종적으로 데이터의 일관성을 맞춰지게 되어, 분산락을 사용하지 않으면서 데이터 일관성 보장할 수 있었습니다.
/*
* 1. [A 트랜잭션] 재고 조회 : 2개
* 2. [B 트랜잭션] 재고 조회 : 2개
* 3. [A 트랜잭션] 실제 주문량(2개) 만큼 재고 감소 : 현재 재고량(2) -> 차감 후 재고량(0)
* 4. [B 트랜잭션] 실제 주문량(1개) 재고 감소 : 현재 재고량(0) -> 차감 후 재고량(-1) 개 // 🧨 차감이 되지 않아야 하는데, 차감 됨
* 5. [B 트랜잭션] 실제 주문량(1개) 만큼 rollback // ✨ 차감되지 않아야 했기 때문에, 재고 원상 복귀 시킴
* /