Repository navigation
stock gate per product
hyoguoo edited this page Sep 5, 2026
·
1 revision
Redis Cluster 도입을 위해 결제 단위 원자 Lua 스크립트를 상품 단위로 쪼개고 잃은 보장을 애플리케이션 레벨에서 다시 세운 기록
| 용어 | 짧은 풀이 |
|---|---|
| 해시 슬롯 | Redis Cluster가 키를 분산 저장하는 단위 — 키 이름을 해싱하여 배치할 슬롯 결정 |
| 해시태그 | 키 이름 중 중괄호 {}로 감싼 부분 — 해당 부분만 해싱하여 동일 태그를 같은 슬롯에 배치 |
| 선점 | 동일 주문의 중복 요청 방지 — 캐시에 마커를 남기는 동작 |
| TX | 트랜잭션 — 모두 반영되거나 모두 취소되는 논리적 작업 단위 |
| 미결 차감 기록 | 결제 DB에 상품마다 남기는 차감 흔적 — 캐시 롤백 실패 시 회수 근거 데이터 |
| 잡음 | 차감 시도를 기록했으나 확정 및 복구되지 않은 미결 상태(NOISE) |
| 회수 | 만료·실패 결제의 게이트 미결 차감을 주기적(Batch)으로 되돌리는 동작 |
초기 모델은 한 결제의 모든 상품 키를 스크립트 하나에 담아 원자 처리했다.
- Redis Cluster 환경은 다중 키 조작 시 모든 키가 동일한 해시 슬롯에 존재해야 함
- 주문에 담긴 상품 A와 B의 재고 키는 서로 다른 슬롯에 배정될 확률이 높아 기존 주문 단위 여러 상품 스크립트는 실행 불가
- 주문마다 상품 조합이 가변적이라 슬롯 사전 정렬 불가
- 결제 단위 원자성을 유지하려면 캐시를 단일 노드로 운영해야 하는 제약 발생
원자성 경계를 상품 단위로 내리기 위해 선차감 게이트의 한계를 수용하고 역할을 명확히 재정의했다.
- 과대 평가 차단: 캐시 오차로 인해 실제 재고를 초과하여 판매 (초과 판매)하는 치명적 결함을 원천 배제
- 과소 평가 수용: 상품 순회 도중 예외가 발생하면 롤백 전까지 캐시 재고가 실제보다 적게 표시되는 현상 (Underselling)을 일부 수용
- 기회비용 한정: 과소 평가로 인해 일시적으로 판매를 못 하는 기회비용은 주기적인 백그라운드 회수 (Batch)를 통해 안전하게 복구
- 역할: 캐시 게이트는 무거운 RDB로 가기 전 트래픽을 방어하는 빠른 거절 장치 (Fast Fail)로 활용
- 한 상품에 딸린 세 키를 상품 번호 (productId) 해시태그로 묶음
- 클러스터 환경에서도 단일 상품에 대한 스크립트 원자적 실행 보장
- 주문 번호를 해시태그로 쓰면 동일 상품의 재고 키가 파편화되므로 상품 번호 기준 채택
| 키 | 역할 |
|---|---|
stock:{productId} |
상품별 게이트 재고 잔량 |
decrement:done:{productId}:orderId |
해당 주문이 상품을 차감했다는 표시 마커 |
compensation:done:{productId}:orderId |
해당 주문이 상품을 롤백했다는 표시 마커 |
Lua 스크립트 역시 한 번에 하나의 상품만 원자적으로 다루도록 역할을 축소 및 재편했다.
| 스크립트 | 책임 | 반환 |
|---|---|---|
stock_decrement_atomic |
상품 하나 선차감 | ALREADY_DONE / INSUFFICIENT / OK |
stock_compensation_if_decremented |
선차감 흔적이 있을 때만 되돌리기 | NO_DECREMENT / ALREADY_DONE / OK |
stock_reject_compensation |
거절 시 재고 복구 및 두 표시 삭제 | OK |
stock_order_lock_acquire |
주문 단위 선점 획득 | OK / LOCKED |
stock_order_lock_release |
토큰이 일치할 때만 선점 해제 | 삭제된 키 수 |
- 선차감 원자성 보장 (
stock_decrement_atomic): 스크립트 내부에서 차감 완료 마커 (decrement:done)를SETNX로 먼저 세운 뒤 재고 잔량을 확인 - 잔량 부족 시 마커를 지우고
INSUFFICIENT를 반환하여 상태 불일치를 원천 차단
flowchart TD
A([확정 요청]) --> B{주문 단위 선점 락}
B -->|" 실패 "| C([이미 처리 중 - 결제 상태 보존 후 거절])
B -->|" 성공 "| D{남은 상품 반복 처리}
D -->|" 있음 "| E{RDB 장부 확정 여부}
E -->|" 확정됨 "| D
E -->|" 미결 "| F["장부에 미결(NOISE) 기록 후 캐시 차감"]
F --> G{차감 결과}
G -->|" 충분 / 이미 차감됨 "| D
G -->|" 부족 "| H["현재 호출로 차감 성공한 상품만 롤백"]
G -->|" 인프라 예외 "| I([차감 성공분 롤백 시도 후 격리])
D -->|" 없음 "| J([결제 진행 상태로 전이])
H --> K([거절 응답])
기존 단일 스크립트가 보장하던 동시성 제어 및 원자성을 애플리케이션 레벨에서 명시적으로 복원했다.
- 도입 배경: 애플리케이션 주도의 롤백 도중, 개입한 후행 요청이 롤백된 재고를 탈취해버리는 동시성 문제 차단
- 해결책: 상품 순회 전체 구간을 감싸는 주문 단위 락 (
acquireOrderLock)을 신설하여 동일 주문의 동시 진입 통제
부분 차감 거절로 인한 롤백 시, 차감 마커와 롤백 마커를 모두 제거하여 깨끗한 상태로 재시도를 수용할 수 있도록 했다.
- 선차감 마커 방치 시: 재시도 트랜잭션이 이미 차감된 것으로 오인하여 재고 차감 없이 결제를 승인하는 버그 발생
- 롤백 마커 방치 시: 재시도가 만든 새로운 차감분을 추후 주기적 회수 배치가 롤백 처리된 것으로 오인해 돌려놓지 못함
상품 단위 순회와 차감 로직은 외부 네트워크 (Redis) 통신을 수반하므로, 데이터베이스 커넥션 점유 시간을 최소화하기 위해 트랜잭션 구간을 명확히 분리했다.
sequenceDiagram
participant O as 결제 오케스트레이터
participant DB as 결제 데이터베이스
participant Redis as Redis 캐시
Note over O,Redis: 1. 분산 락 획득 (DB 커넥션 미사용)
O ->> Redis: 결제 요청(주문 단위) 제어권 획득 (동시 요청 방어)
loop 주문에 포함된 각 상품마다 반복
Note over O,DB: 2. 단기 트랜잭션 (TX 1)
O ->> DB: 차감 이력 사전 기록 (상태: 미결/차감 중)
DB -->> O: 즉시 커밋 후 DB 커넥션 반납
Note over O,Redis: 3. 네트워크 통신 (DB 커넥션 반납 상태)
O ->> Redis: 상품 단위 캐시 재고 차감 시도
end
O ->> Redis: 분산 락 해제
Note over O,DB: 4. 메인 트랜잭션 (TX 2)
O ->> DB: 결제 진행 상태(IN_PROGRESS) 갱신 및 이력 확정 처리
DB -->> O: 최종 커밋 후 DB 커넥션 반납
- 사후 복구 보장: Redis 통신 중 프로세스가 강제 종료되더라도, 디스크에 선행 커밋된 미결 레코드를 근거로 스케줄러가 누락된 롤백을 안전하게 수행
- 커넥션 풀 고갈 방지:
TX 1 커밋 -> Redis I/O -> TX 2 커밋순서로 분리하여, 캐시 통신 지연이 RDB 커넥션 풀을 장시간 점유하는 현상 원천 차단
- 롤백 호출 자체가 인프라 장애로 실패하면 게이트에 차감 방치
- 회수 근거를 위해 결제 DB에 미결 차감 기록 (stock_hold_record) 선행 저장
백그라운드 배치가 60초 주기로 만료 및 실패 처리된 결제의 미결 (NOISE) 기록을 조회하여 롤백 및 장부 마감 처리하도록 했다.
stateDiagram-v2
direction TB
[*] --> NOISE: 차감 시도 전 기록
NOISE --> COMMITTED: 승인 반영 - 결제 완료 전이 TX와 묶임
NOISE --> REVERTED: 되돌리기 성공 또는 흔적 없음
COMMITTED --> COMMITTED: 재요청이 와도 건드리지 않음
| 원칙 | 위반 시 문제점 |
|---|---|
| 캐시 차감 전 DB 장부 선행 기록 | 차감 후 크래시 발생 시 롤백 대상을 식별할 수 없어 재고가 영구적으로 묶임(Leak) |
| 결제 승인 트랜잭션 내에서 상태 확정 | 종결과 확정 마킹 사이에 배치가 개입하여 실제 판매된 재고를 강제 롤백하는 치명적 버그 발생 |
| 확정된 결제는 캐시 통신 완전 배제 | 단기 캐시 마커(8일) 만료 후 재시도 요청 유입 시, 기판매 상품에 추가 차감이 발생하는 오류 |
- stock-cache-recovery — 본 문서가 갈아엎은 Phase 6 초기 단일 Lua 제어 모델
- event-driven-choreography — 확정 통지가 product-service로 가는 경로
- message-delivery-and-dedupe — DLQ 격리 토픽 및 소비 멱등성 일반론
- architecture — 서비스 토폴로지와 캐시 배치
🏠 Home
- MSA 전환
- 이벤트 드리븐 코레오그래피
- 재고 캐시 보상 회복 — Lua atomic
- Outbox 패턴
- 메시지 전달 보장 + dedupe
- 결제 복구 모델
- PG 결제 확인 흐름
- 분산 트레이싱
Phase 1 · 데이터 정합성 확립
Phase 2 · 결합도 해소 및 자가 복구력
Phase 3 · 운영 가시성 및 안정성
Phase 4 · 데이터 정합성 심화 및 중복 제어
Phase 5 · 비동기 결제 아키텍처 전환
ETC · 설계 유연성
ETC · AI 기반 개발 워크플로우