TIL

06. Redis 재고 관리 방식

동시성 문제의 본질

재고 차감 원자성 보장 방식

① 단순 카운터 (INCR / DECR)

DECR stock:{sku}        # 일단 차감
# 반환값이 음수면 오버셀 → INCR stock:{sku} 로 복구 후 거부

② Lua 스크립트

-- KEYS[1]=재고키, ARGV[1]=차감 수량
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
  return redis.call('DECRBY', KEYS[1], ARGV[1])  -- 차감 후 잔량 반환
end
return -1                                          -- 재고 부족

③ 낙관적 트랜잭션 (WATCH / MULTI / EXEC)

WATCH stock:{sku}        # 키 변경 감시 시작
val = GET stock:{sku}    # 현재 재고 읽기
# 애플리케이션에서 val >= 수량 검증
MULTI
DECRBY stock:{sku} qty
EXEC                     # 감시 키가 변경됐으면 nil → 재시도(보통 최대 N회)

④ 이중 카운터 (Double Counter)

⑤ Set 자료구조 (구매번호를 원소로)

SADD   {product}:stock {orderNo}   # 구매: 주문번호 추가 (중복 시 0 반환 = 멱등)
SREM   {product}:stock {orderNo}   # 취소: 주문번호 제거 (멱등)
SCARD  {product}:stock             # 사용량 O(1) 조회

⑥ 분산 락 (Redisson / SETNX)

SET lock:{sku} {uuid} NX PX 3000   # 락 획득(없을 때만, TTL 3초)
# ... 재고 조회·차감 임계영역 ...
# 해제는 Lua로 "값이 내 uuid일 때만 DEL" (남의 락 해제 방지)

⑦ 토큰 게이트 / 리스트 (RPUSH → LPOP)

RPUSH tokens:{sku} t1 t2 ... tN    # 재고 N개 = 토큰 N개 사전 적재
LPOP  tokens:{sku}                 # 구매: 토큰 1개 소비, nil이면 품절

방식 비교

방식 핵심 원리 멱등성 주요 약점
① 단순 카운터 DECR 선차감 + 음수 시 보상 중복 이벤트 시 과차감
② Lua 스크립트 check+차감을 서버에서 원자 실행 읽기분산 불가, 처리량 저하
③ 낙관적 트랜잭션 WATCH 변경 시 EXEC 취소 → 재시도 고경합 retry storm
④ 이중 카운터 요청 카운터로 경합 선차단 키 2개 정합 관리
⑤ Set(구매번호) 잔량 = 정원 − SCARD 집합 메모리 증가
⑥ 분산 락 락 획득 후 임계영역에서 차감 직렬화 성능, 락 정확성
⑦ 토큰/리스트 토큰 N개를 LPOP으로 소비 △ 오버셀 불가 사전 적재, 가변 재고 부적합

Redis ↔ DB 정합성

확장성 — Hot Key 문제

반대 관점 — Redis 대신 DB (Shopify)

참고