Shopify는 재고 예약을 MySQL로 옮겼습니다
Shopify는 재고 예약 시스템을 Redis에서 이미 재고 원장을 보유하고 있던 MySQL 데이터베이스로 옮겼습니다.
Shopify는 재고 예약 시스템을 Redis에서 이미 재고 원장을 보유하고 있던 MySQL 데이터베이스로 옮겼습니다. 개별적으로 잠글 수 있는 유닛 행의 경계가 있는 풀을 통해 동시 결제가 SKIP LOCKED를 사용하여 다른 적격 유닛을 선택할 수 있습니다. 이 설계는 또한 트랜잭션 경계, 기본 키 레이아웃, 보충 규칙 및 결제 전반에 걸친 연결 유지 시간 관찰에 따라 달라집니다.
이 동영상에서 다루는 내용
- 이전 Redis 수량 카운터 모델은 동시성을 처리했지만, 예약 정리 및 MySQL 원장 업데이트는 단일 로컬 원자성 트랜잭션을 공유할 수 없었습니다.
- 교체 시스템은 품목/위치 조합당 1,000개로 제한된 풀에 사용 가능한 단위당 하나의 행을 사용합니다. 빈 풀은 보충 잠금 뒤에서 동시 요청이 대기하는 인라인 보충을 트리거할 수 있습니다.
- 복합 기본 키는 (shop_id, inventory_item_id, inventory_group_id, id)입니다. Shopify는 프로토타입에서 잠금 오버헤드를 줄였고, 보충을 차단하는 갭 잠금을 피하기 위해 READ COMMITTED를 사용했습니다.
- 예약은 예약 레코드를 삽입하기 전에 풀 행을 삭제합니다. 커밋은 데이터베이스 잠금을 해제하는 반면, 저장된 보류는 결제 전반에 걸쳐 유지됩니다. 성공적인 결제는 원장을 청구하고 나중에 원자성 트랜잭션으로 예약을 제거합니다.
- MySQL 문서는 SKIP LOCKED를 잠긴 행을 생략하는 일관성 없는 뷰로 설명합니다. 이는 완전한 재고 수량 또는 공정한 차례를 설정하지 않으며, 행 수준 잠금에만 적용되고, 문 기반 복제에는 안전하지 않습니다.
- 소스 예제에는 expires_at이 포함되어 있지만, Shopify는 MySQL 만료-정리 알고리즘을 문서화하지 않습니다. 비디오의 릴리스/만료 분기는 설명적 라이프사이클 요구사항입니다.
- 다른 결제 코드의 연결 유지 시간이 최종 처리량 병목 현상이었습니다. Shopify는 두 시스템을 섀도우 라이트하고, 결과를 비교하고, Redis 킬 스위치 폴백으로 점진적으로 전환했습니다.
번역된 스크립트
원래 영어 내레이션에서 번역되었습니다. 사용 가능한 오디오 및 캡션은 YouTube에서 제어합니다.
예약을 MySQL로 옮긴 이유는 무엇입니까?
0:00 결제는 빠르게 유지되기 위해 Redis가 필요합니다. Shopify는 재고 예약을 MySQL로 옮겼는데, 경계가 있는 풀에 단위당 하나의 행을 사용했습니다. 왜 MySQL을 선택했을까요? 잠금을 안전하게 건너뛰는 방법은 무엇입니까? 여기는 The Daily Diff, 내부의 모습입니다. 그리고 빠른 쿼리가 왜 여전히 한계에 부딪혔을까요? Shopify는 온라인 및 직접 판매를 위한 상거래 플랫폼입니다. Redis는 인메모리 데이터 저장소입니다.
0:21 MySQL은 관계형 데이터베이스이며, Shopify는 이미 재고 원장을 그곳에 보관하고 있었습니다. 예약은 구매자가 결제하는 동안 잠시 재고를 보류합니다.
두 개의 저장소가 위험했던 이유는 무엇입니까?
0:29 그들의 Redis 모델은 품목 카운터를 감소시켰습니다. 결제된 주문을 청구하는 것은 MySQL 원장을 업데이트하고 Redis를 정리하는 것을 의미했습니다. 이러한 별도의 쓰기는 재고가 두 번 판매되거나 판매 가능해야 할 때 사용할 수 없게 만들 수 있었습니다. 이전 모델은 위치 인식도 부족했습니다. 교체 시스템은 주문을 이행할 수 있는 곳에서 재고를 선택해야 합니다. 잘못된 대륙에 있는 창고는 훌륭한 데이터베이스 항목이지만, 끔찍한 배송 약속이 됩니다. 이전 MySQL 시도는 수량 행을 사용했기 때문에 경쟁하는 결제가
잠글 수 있는 단위는 무엇이 됩니까?
0:56 동일한 잠금에서 대기했습니다. 스프레드시트 셀 주변의 벨벳 로프를 생각해보세요. 더 많은 작업자를 추가하는 것은 단지 대기열을 늘릴 뿐입니다. Shopify는 잠글 수 있는 객체를 변경했습니다. 각 사용 가능한 단위는 자체 행을 얻습니다. 잠금 읽기는 다른 트랜잭션이 보유하는 단위를 건너뛰고, 다른 적격 단위를 선택합니다. 다른 작업자는 다른 행을 획득할 수 있으며, 핫 카운터는 그들의 방해가 되지 않습니다.
풀이 비워지면 어떻게 됩니까?
1:15 그 풀은 품목 및 위치당 천 개의 행으로 제한됩니다. 보충은 원장에서 가져옵니다. 만약 비워지면, 예약 경로는 보충 잠금 뒤에서 경쟁하는 요청이 대기하는 인라인으로 보충됩니다. 빈 풀은 빈 창고를 의미하지 않습니다. 예약은 선택된 풀 행을 삭제한 다음, 트랜잭션에서 예약 레코드를 삽입합니다. 커밋은 데이터베이스 잠금을 해제합니다. 롤백은 변경 사항을 되돌립니다.
1:35 예약은 저장된 상태로 결제 처리 후에도 유지됩니다. 성공적인 결제는 원장을 청구하고 예약을 원자적으로 제거합니다. 데이터베이스 잠금은 결제 양식을 돌볼 필요가 없습니다. 그들의 복합 기본 키는 상점, 품목, 그룹, 그 다음 단위 식별자로 시작합니다. 조회 일치는 프로토타입에서 인덱스 잠금을 줄였습니다. 그들은 또한 풀 보충을 차단하는 갭 잠금을 피하기 위해 read committed를 사용하고, 원형 대기를 방지하기 위해 일관된 테이블 순서를 사용합니다.
1:57 공개된 예제는 만료 시간을 기록합니다. 포기된 결제는 결국 재고를 해제해야 합니다. 그렇지 않으면 쇼핑 카트가 집주인이 됩니다. Shopify의 게시물은 해당 정리 알고리즘을 지정하지 않았으므로, 이 다이어그램은 라이프사이클 요구사항을 보여줍니다. 여기에 문제가 있습니다. SKIP LOCKED는 잠긴 행을 제외하므로, 매뉴얼은 그 결과를 일관성 없는
SKIP LOCKED가 생략하는 것은 무엇입니까?
2:14 뷰라고 부릅니다. 이는 완전한 재고 수량이나 공정한 차례를 제공하지 않습니다. 가용성 결정과 보충 규칙을 그 주변에 유지하세요. 그리고 그 한계는요?
실제 상한선은 어디였습니까?
2:26 다른 결제 코드가 데이터베이스 연결을 너무 오래 유지하고 있었습니다. Shopify는 호출자를 태그하고 연결 유지 시간을 측정했으며, 그 다음 결제 경로를 정리하고 스레드 동시성을 다시 검토했습니다. 빠른 쿼리도 쿼리 외부에서 여전히 대기할 수 있습니다. 그들은 Redis가 권한을 가진 상태로 두 시스템을 섀도우 라이트하고,
왜 이 설계를 출시해야 합니까?
2:38 결과를 비교한 다음, 킬 스위치로 점진적으로 전환했습니다. 제 평결은 SHIP IT입니다. 저는 전체 결제 경로에 계측 기능을 추가하여 공유 트랜잭션 경계와 롤백 가능한 롤아웃을 출시할 것입니다. 이것에 대해 질문이 있습니까? 댓글에 남겨주세요. 이것으로 오늘의 diff는 끝입니다. 저는 Axrisi의 Niko입니다.
2:54 책임감 있게 병합하세요. 책임감 있게 병합하세요.
출처
- We replaced Redis with MySQL for inventory reservations—and it scaledShopify Engineering — Emilie Noel
- Simplified reservation SQL embedded in Shopify's articleShopify Engineering / CourtneySymons on GitHub Gist
- MySQL 8.0 — Locking ReadsOracle / MySQL Reference Manual
- MySQL 8.0 — Transaction Isolation LevelsOracle / MySQL Reference Manual
- What Is Shopify and How Does It Work?Shopify
- Redis quick startsRedis documentation
- What is MySQL?Oracle / MySQL Reference Manual



