Shopify는 2026년 5월 12일 재고 예약 시스템을 Redis에서 MySQL로 옮긴 과정을 공개했다. 결제 처리 중인 주문이 같은 마지막 재고를 동시에 가져가지 못하게 하는 짧은 예약을, MySQL 8의 SKIP LOCKED와 단위별 행 모델로 처리한 것이다. 핵심은 캐시를 데이터베이스로 바꾼 일이 아니라, 예약과 실제 재고 원장을 하나의 ACID 트랜잭션 안에 넣고 잠금 경쟁의 단위를 다시 설계한 데 있다.
이 문제는 결제 성공 뒤 재고를 확정 차감하는 순간에 드러난다. 이전 Redis 모델에서는 상품별 수량 키를 DECR로 줄이고 해제 시 INCR로 되돌렸다. 동시성 자체는 Redis가 잘 처리했지만, 예약은 Redis에, 재고 원장은 MySQL에 있었다. 결제 완료 시 MySQL 원장 갱신과 Redis 예약 정리를 한 번에 원자적으로 끝낼 수 없으므로 실행 순서가 어긋나면 판매됐지만 원장에 차감되지 않거나, 원장에서는 차감됐는데 예약이 남는 상태가 생길 수 있었다. 여러 출고 위치 중 실제로 이행 가능한 곳을 골라야 하는 다중 위치 재고도 별도 모델이 필요했다.

상품 수량 하나가 아니라 판매 가능한 단위 행을 고른 이유는 무엇인가
Shopify는 수량 열 하나를 갱신하는 대신 판매 가능한 재고 단위마다 행 하나를 두었다. 재고가 10개면 행도 10개이고, 3개를 예약하면 세 행을 골라 예약 테이블로 옮긴다. 예약 쿼리는 SELECT … FOR UPDATE SKIP LOCKED로 이미 다른 트랜잭션이 잠근 행은 기다리지 않고 건너뛴다. 동시에 결제하는 두 주문이 같은 행을 잡으려 해도, 다음 사용 가능한 행을 선택하므로 한 수량 행에 요청이 몰리는 방식보다 경합을 줄인다.
단위별 행을 재고 전체에 무제한으로 만들지는 않았다. 원문이 든 예처럼 재고 5만 개를 10개 위치에 보유한 상품이면 50만 행이 되어 스캔 비용이 커진다. 그래서 Shopify는 상품·위치 조합마다 사용 가능한 행 풀을 최대 1,000개로 제한하고, 예약으로 줄어든 풀은 재고 원장에서 보충한다. 폭주 때 풀이 비면 예약 경로가 보충을 직접 시작하며, 하나의 트랜잭션만 보충하도록 잠가 같은 상품에 여러 보충 작업이 몰리는 일을 막는다.

잠금 수와 격리 수준은 어떻게 맞췄나
이 설계는 쿼리 문법 하나만으로 끝나지 않았다. 자동 증가 ID를 기본 키로 쓴 첫 프로토타입에서는 예약 한 건에 행 잠금이 두 개 생겼다. 조건절에 쓰는 보조 인덱스와 클러스터드 인덱스가 모두 잠겼기 때문이다. Shopify는 shop_id, inventory_item_id, inventory_group_id, id를 묶은 복합 기본 키로 바꿔 해당 조건이 기본 키에 포함되게 했고, 예약당 잠금을 하나로 줄였다.
빈 풀을 보충할 때는 MySQL 기본 격리 수준인 REPEATABLE READ에서 갭 잠금이 새 행 삽입을 막아 교착 상태로 이어졌다. 예약 트랜잭션을 READ COMMITTED로 바꿔 보충이 진행되게 했고, 예약과 확정 차감이 두 테이블을 만지는 순서도 통일했다. 여러 상품을 담은 장바구니의 예약 쿼리는 UNION ALL로 묶어 왕복 횟수도 줄였다. 이 대목은 데이터 모델, 인덱스, 격리 수준, 잠금 순서가 같은 처리량 목표를 함께 결정한다는 사례다.
최적화한 예약 쿼리보다 먼저 봐야 했던 것은 무엇인가
운영 부하에서는 예약 지연의 P90이 허용 범위이고 CPU도 포화되지 않았는데 처리량 상한이 목표보다 낮게 나타났다. Shopify가 찾아낸 병목은 예약 SQL 자체보다 데이터베이스 연결이었다. 애플리케이션이 모든 SQL에 업무 프로세스를 식별하는 주석 태그를 붙이고, ProxySQL이 이를 읽어 호출자별 연결 점유 시간을 집계하게 하자 장시간 연결을 잡고 있던 결제 경로의 다른 작업이 드러났다.

그 경로를 정리한 결과 원문 기준으로 주 데이터베이스의 읽기는 50%, 트랜잭션은 33% 줄었다. 이어 InnoDB 스레드 동시성 설정을 현재 부하에 맞춰 조정하자 플래시 세일에서 쓰기 CPU는 50% 미만, 읽기 CPU는 16% 미만으로 유지됐다. 전환도 즉시 바꾸지 않았다. Redis를 기준으로 둔 채 두 저장소에 같은 예약을 기록하는 섀도 모드로 실제 트래픽에서 결과와 성능을 비교했고, 이후 낮은 트래픽의 파드부터 MySQL을 기준 저장소로 바꿨다. 이 사례의 결론은 Redis가 부적절하다는 선언이 아니다. 분산된 상태 때문에 원자성이 끊기는 문제, 한 수량 행의 잠금 경합, 연결 풀의 실제 점유 주체를 각각 분리해 측정했을 때 기존 관계형 데이터베이스가 더 단순하고 안전한 경로가 될 수 있다는 뜻이다.
출처: Shopify Engineering, “We replaced Redis with MySQL for inventory reservations, and it scaled” (2026-08-09 확인)
