Shopify, 재고 예약 시스템을 Redis에서 MySQL로 교체
요약
Shopify는 오버셀 방지를 위해 기존 Redis 기반 재고 예약 시스템을 MySQL로 교체했습니다. MySQL 8의 SKIP LOCKED 기능을 활용해 단위당 1행 구조로 재설계함으로써 데이터 일관성을 확보하고 성능을 최적화했습니다.
핵심 포인트
- Redis와 MySQL 간 데이터 불일치 문제를 ACID 트랜잭션으로 해결
- SKIP LOCKED를 활용해 락 경합 및 데드락 최소화
- 판매 단위당 1행 구조 채택으로 고성능 처리 달성
- 복합 기본 키 적용을 통해 인덱스 잠금 오버헤드 감소
- 블랙프라이데이 피크 시에도 안정적인 CPU 사용률 유지
재고 예약 시스템은 결제 처리 중 동일 상품이 두 번 판매되는 오버셀을 방지하는 핵심 인프라로, Shopify는 수년간 Redis 기반으로 운영해왔음
- MySQL 8의
SKIP LOCKED
기능을 활용해 아이템당 수량 컬럼 대신 판매 단위당 1개 행 구조로 재설계, Redis 없이도 고성능 처리 달성
복합 기본 키, READ COMMITTED
격리 수준, 일관된 잠금 순서, UNION ALL
배치 처리 등 MySQL 최적화 기법을 조합해 락 경합과 데드락을 해소
- 실제 병목은 예약 쿼리가 아닌
커넥션 점유에 있었으며, 체크아웃 경로 전체를 계측해 DB 읽기 50%, 트랜잭션 33% 감소 달성
2025년 블랙프라이데이 피크 기준 분당 $510만 매출을 처리하면서 writer CPU 50% 미만, reader CPU 16% 미만을 유지하며 목표 처방량 초과 달성
배경: 오버셀 방지 시스템의 요구사항
-
체크아웃 완료 시점에 재고가 실제로 남아있음을 보장하는
오버셀 방지(Oversell Protection) 시스템이 필요
Reserve: 결제 시작 시 수 분간 해당 아이템을 임시 잠금
Claim: 결제 완료 시 재고 원장에서 수량을 영구 차감 -
두 방향 모두에서 오류 허용 불가
-
잘못되면 동일 상품을 두 명이 구매하거나, 재고가 있음에도 품절 처리되어 매출 손실 발생
규모 요건: Shopify는 미국 이커머스의 14% 이상을 담당하며, 2025년 블랙프라이데이에는 전년 대비 11% 증가한 분당 $510만 매출 기록
- 다중 위치 재고(Multi-location inventory), ACID 보장, 고성능 처리량, 정확성 우선이 핵심 요건
기존 Redis 모델의 한계
- Redis에서 각 아이템은 수량 키를 가지며, 예약은
DECR
, 해제는 INCR
로 처리
핵심 문제: 예약 데이터(Redis)와 재고 원장(MySQL)이 서로 다른 시스템에 존재
-
Claim 단계에서 MySQL 업데이트와 Redis 정리를 단일 원자적 트랜잭션으로 묶을 수 없었음
-
실행 순서에 따라 오버셀(상품 판매됐으나 원장 미차감) 또는 언더셀(원장 차감됐으나 여전히 예약 상태) 발생 가능
-
다중 위치 재고 인식 기능 부재, 별도 Redis 클러스터 운영 비용 부담
핵심 해법: SKIP LOCKED 기반 MySQL 재설계
기본 구조: 단위당 1행(One Row Per Unit)
-
아이템당 수량 컬럼 대신
판매 가능 단위당 1개 행 구조 채택 -
재고 10개짜리 아이템 → 10개 행; 3개 예약 시 단일 트랜잭션에서 3개 행을 선택·이동
-
예약과 재고 원장을 동일 MySQL DB에 두어 reserve와 claim을
ACID 트랜잭션으로 처리, Redis에서 발생하던 버그 유형 제거
SKIP LOCKED
: 다른 트랜잭션이 잠근 행은 건너뛰고 가용 행을 즉시 반환 → 동일 행 대기 없이 경합 감소
풀(Pool) 크기 제한: 위치별 최대 1,000행
-
아이템/위치 조합당 가용 행을 최대
1,000개로 제한해 테이블 크기와 스캔 성능 유지 -
예: 50,000개 재고 × 10개 위치 = 500,000행이 되는 상황 방지
-
풀 소진 시
인라인 보충(replenishment) 트리거; 단일 트랜잭션만 보충하도록 락을 걸어 다수 트랜잭션이 동시에 행을 삽입하는 thundering herd 방지 -
풀이 완전히 비는 경우 해당 예약에만 지연 발생, 실제 재고가 있는 구매자가 품절 처리되는 일은 없음
핵심 기술 결정 4가지
1. 복합 기본 키로 락 수 절감
- 초기 프로토타입에서 오토인크리먼트 ID를 기본 키로 사용 시, InnoDB가 보조 인덱스와 클러스터드 인덱스 양쪽을 잠가
예약당 2개 행 락 발생
shop_id, inventory_item_id, inventory_group_id, id
로 구성된 복합 기본 키 적용 → 필터 컬럼이 기본 키에 포함되어 락이 1개로 감소
- 초당 수천 건 예약 환경에서 인덱스·기본 키 설계가 락 수와 처리량에 직접 영향
2. READ COMMITTED로 갭 락 제거
- 빈 테이블에
SELECT ... FOR UPDATE SKIP LOCKED
실행 시 갭 락(supremum 포함) 발생, 보충 트랜잭션의 INSERT를 차단하고 데드락 유발
- 격리 수준을 MySQL 기본값인
REPEATABLE READ
에서 READ COMMITTED
로 변경 → 갭 락 발생 방식이 달라져 보충 트랜잭션이 정상 진행
- 해당 코드베이스 최초의 비기본 격리 수준 적용으로, 트랜잭션별 격리 수준 설정을 위한 소규모 프레임워크 지원 필요
3. 일관된 락 순서로 데드락 방지
- reserve와 claim이 두 테이블을
다른 순서로 접근해 데드락 발생 - reserve:
reserved_quantities
INSERT → reservation_units
DELETE
- claim:
reserved_quantities
DELETE
- 해결책: reserve가 항상 units 테이블 DELETE 먼저,
reserved_quantities
INSERT 나중으로 순서 표준화 → 순환 대기(circular wait) 제거
4. UNION ALL 배치로 라운드트립 감소
- 장바구니에 여러 라인 아이템이 있을 때
UNION ALL
로 예약 쿼리를 단일 라운드트립으로 배치 처리
- 총 라운드트립 감소로 부하 상황에서 레이턴시 개선
실제 병목: 쿼리가 아닌 커넥션 점유
문제 발견 과정
- 운영 환경에서 목표 처리량 이하에서 천장에 도달, P90 레이턴시는 양호, CPU는 최대 미만, 쿼리도 최적화 완료 상태
- 부하 테스트에서 관찰된 증상:
- MySQL 내 스레드 큐잉
- 큐에 쌓인 작업 실행 시 CPU 급등
- ProxySQL 레이어에서 MySQL 백엔드 커넥션 고갈
커넥션 가시성 확보
- 애플리케이션 레이어: 모든 SQL 구문에
/* conn_tag:checkout_completion */
형태의 비즈니스 프로세스 식별 주석 추가
- ProxySQL 레이어: 태그 파싱 및 호출자별
커넥션 점유 시간 집계 추가 - 결과: 어떤 프로세스가 얼마나 오래 커넥션을 점유하는지 즉시 파악 가능
발견 내용과 해결
-
예약 외
체크아웃 경로의 다른 코드들이 커넥션을 필요 이상으로 길게 점유 중이었음 -
이들이 먼저 한계에 도달하지 않아 최적화 대상에서 누락됐던 코드
-
체크아웃 경로 정리 결과:
프라이머리 DB 읽기 50% 감소, 트랜잭션 33% 감소 -
수년 전 보수적으로 설정된 후 재검토하지 않았던
InnoDB 스레드 동시성 설정 조정으로 추가 병목 제거 -
개선 후 고볼륨 플래시 세일 기준: writer CPU 50% 미만, reader CPU 16% 미만 유지
전환 방식: Shadow Mode
-
Redis에서 MySQL로 즉시 전환하지 않고,
Shadow Mode 방식으로 두 시스템 병렬 운영 -
모든 예약을 Redis와 MySQL 양쪽에 동시 기록, Redis가 source of truth 유지
-
실제 운영 트래픽에서 MySQL의 정확성과 성능을 병렬 검증
-
인플라이트 예약 마이그레이션 없이 전환 가능 (양쪽 시스템이 동시에 살아있었으므로)
-
MySQL로 source of truth 전환 후에도
킬 스위치 유지, 이중 쓰기 경로를 통해 Redis가 항상 최신 상태 보존 -
롤아웃은 낮은 트래픽 파드부터 최고 볼륨 머천트까지
점진적으로 파드 단위 진행
교훈
1. 오래된 결정을 재검토할 것
- 5년 전에는 불가능했던 MySQL 활용이
SKIP LOCKED
같은 신규 기능으로 현재는 가능
- 스레드 한도 등 "경험칙" 설정은 워크로드와 하드웨어가 변화하면 재검토 필요
- CPU는 낮은데 큐잉이 발생한다면 반드시 원인을 파야 함
2. 작게 시작하고 관찰할 것
- 풀 Rails 프레임워크 없이 소규모 Ruby 스크립트와 MySQL로 최소 프로토타입 구성
- 두 번째 터미널에서 락 동작을 직접 관찰하는 방식이 이론보다 더 많은 것을 가르쳐줌
커넥션 점유 계측 패턴 (앱 레이어 태그 + 프록시 집계)은 구현이 간단하고 즉시 실행 가능
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: GeekNews (한국어)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기