SELECT ... FOR UPDATE): 같은 재고 row에 모든 주문이 직렬화 → hot row 병목재고 = 1을 읽고, 둘 다 차감 가능하다고 판단 → 둘 다 재고 = 0으로 써서 오버셀DECR 등)은 원자적
DECR/DECRBY로 차감 — 단일 명령이라 순차 처리 보장DECR은 조건부가 아니라 재고 0에서도 음수까지 내려감(오버셀)INCR로 되돌리는 보상 방식DECR stock:{sku} # 일단 차감
# 반환값이 음수면 오버셀 → INCR stock:{sku} 로 복구 후 거부
-- 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 -- 재고 부족
EVALSHA로 단일 RTT 처리WATCH한 키가 EXEC 전에 다른 클라이언트에 의해 바뀌면 트랜잭션 전체가 취소(nil 반환)되고 재시도WATCH stock:{sku} # 키 변경 감시 시작
val = GET stock:{sku} # 현재 재고 읽기
# 애플리케이션에서 val >= 수량 검증
MULTI
DECRBY stock:{sku} qty
EXEC # 감시 키가 변경됐으면 nil → 재시도(보통 최대 N회)
MULTI/EXEC는 격리만 보장하고 명령 실패해도 되돌리지 않음countReq)를 먼저 INCR해 한도 초과 요청을 1차 차단count)로 발급 처리잔량 = 정원 − SCARDSADD {product}:stock {orderNo} # 구매: 주문번호 추가 (중복 시 0 반환 = 멱등)
SREM {product}:stock {orderNo} # 취소: 주문번호 제거 (멱등)
SCARD {product}:stock # 사용량 O(1) 조회
SADD해도 no-op → 비동기 중복 이벤트에도 안전SCARD(확인)와 SADD(차감) 사이의 원자성은 MULTI/EXEC나 분산 락으로 별도 보장 필요SET lock:{sku} {uuid} NX PX 3000 # 락 획득(없을 때만, TTL 3초)
# ... 재고 조회·차감 임계영역 ...
# 해제는 Lua로 "값이 내 uuid일 때만 DEL" (남의 락 해제 방지)
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으로 소비 |
△ 오버셀 불가 | 사전 적재, 가변 재고 부적합 |
stock:{sku}:shard:0 ~ stock:{sku}:shard:31SELECT ... FOR UPDATE SKIP LOCKED로 잠긴 행은 건너뛰고 빈 행만 선점 → hot row 직렬화 제거READ COMMITTED로 gap lock 회피, 일관된 락 순서로 교착 차단