Flash Sale 1만 명이 같은 상품 → DB 직렬화 → 결제 실패
Hot key contention 도메인. 같은 row를 동시 update 시 RDBMS row lock 직렬화 → 처리 못한 요청 backpressure → 앱 thread 가득 → cascading. 해결책 4가지 (Redis INCR / sub-counter / Kafka serial / cell architecture).
상황
Flash sale — 인기 상품 1만 명이 동시 클릭. UPDATE inventory SET qty = qty - 1 WHERE id = 123 를 모두 같이 실행.
결과: DB row lock 직렬화. 한 시점에 1개만 처리. 나머지 9999개 대기 → caller thread 점유 → upstream backpressure → cascading.
시뮬 결과
hot-key-contention preset에서:
- 2000 RPS burst
- MySQL lock acquire 2996회 / wait 3033회 / 동시 wait 38건
- Spring queue 400 가득 → 54% drop (queue-full)
- 도메인 정확: DB는 안 죽지만 (CPU 14%) 앱이 죽음
왜 단순 scale-out 안 되나
App 100대로 늘려도 — 모든 트래픽이 같은 row로 모이면 DB row lock은 1개. 100대 app은 99% 대기. DB shard 100개로 분산해도 같은 product 1개는 한 shard로 — 똑같이 직렬화.
해결책 4가지
1. Redis INCR Counter (가장 흔함)
DECR inventory:123 — Redis single-thread + atomic. 100k+ ops/s. Redis 값이 정답, DB는 비동기 동기화 (eventual).
Trade-off: Redis 죽으면 재고 데이터 손실. AOF persistence + replica 필수.
2. Sub-counter 분산
재고 1000개를 10개 sub-counter로 분산 (각 100개). user_id % 10 으로 라우팅. 각 counter는 다른 row → 병렬 처리.
Trade-off: 재고 종료 임박 시 일부 counter는 0, 다른 counter는 남아있을 수 있음. 마지막엔 통합 필요.
3. Kafka Serial Consumer
모든 차감 요청을 Kafka로 publish. partition key = product_id로 같은 상품은 같은 partition. Consumer 1개가 partition 순서대로 처리 (자연스러운 직렬화).
Trade-off: 사용자 대기 시간 (Kafka lag). 응답은 "ack 받음"만 — 결과는 async 알림. 대용량 flash sale에 적합 (몇 초 lag OK 환경).
4. Cell-based Architecture
시스템을 cell로 분리. 각 cell은 독립 DB + app. user를 cell에 sticky 매핑. 한 product의 재고도 cell별로 사전 할당. cell 내부만 직렬화, cell 사이는 독립.
Trade-off: 인프라 N배. 일반 production보단 amazon 같은 hyper-scale에서 사용.
실 사례 — 좌석 예약
영화 / 콘서트 좌석 예약도 같은 패턴. 인기 콘서트 오픈 시 같은 좌석 row update 1만 명 동시.최선: Redis INCR + 좌석 별 sub-counter. 예매 사이트들이 "좌석 일시적으로 잡혔습니다" 메시지 띄우는 이유.
면접 답변 템플릿
"Flash sale의 문제는 scale-out으로 안 풀린다는 점입니다. DB row lock이 자연스러운 직렬화 지점이라 instance를 늘려도 같은 row를 기다리니까 의미가 없습니다.
해결책 4가지 중 일반적으로 Redis INCR counter를 씁니다. 단점은 Redis 죽었을 때라 AOF + replica 필수. 대용량 (수만 RPS+)이면 Kafka serial consumer로 가서 사용자 ack만 즉시 반환, 실제 처리는 async."