Skip to content

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 해당 주문이 상품을 롤백했다는 표시 마커

Redis 제어 스크립트

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([거절 응답])
Loading

단일 스크립트 분할에 따른 보완 로직

기존 단일 스크립트가 보장하던 동시성 제어 및 원자성을 애플리케이션 레벨에서 명시적으로 복원했다.

주문 단위 락 (Distributed Lock)

  • 도입 배경: 애플리케이션 주도의 롤백 도중, 개입한 후행 요청이 롤백된 재고를 탈취해버리는 동시성 문제 차단
  • 해결책: 상품 순회 전체 구간을 감싸는 주문 단위 락 (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 커넥션 반납
Loading

아키텍처 이점

  • 사후 복구 보장: 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: 재요청이 와도 건드리지 않음
Loading

데이터 정합성을 위한 제약 조건

원칙 위반 시 문제점
캐시 차감 전 DB 장부 선행 기록 차감 후 크래시 발생 시 롤백 대상을 식별할 수 없어 재고가 영구적으로 묶임(Leak)
결제 승인 트랜잭션 내에서 상태 확정 종결과 확정 마킹 사이에 배치가 개입하여 실제 판매된 재고를 강제 롤백하는 치명적 버그 발생
확정된 결제는 캐시 통신 완전 배제 단기 캐시 마커(8일) 만료 후 재시도 요청 유입 시, 기판매 상품에 추가 차감이 발생하는 오류

관련 위키

Clone this wiki locally