Shopifyが在庫予約をMySQLに移行
Shopifyは在庫予約システムをRedisから、既に在庫元帳を保持していたMySQLデータベースに移行しました。
Shopifyは在庫予約システムをRedisから、既に在庫元帳を保持していたMySQLデータベースに移行しました。個別にロック可能なユニット行の境界付きプールにより、同時チェックアウトはSKIP LOCKEDを使用して異なる利用可能なユニットを選択できます。この設計は、トランザクション境界、主キーのレイアウト、補充ルール、およびチェックアウト全体での接続保持時間の監視にも依存しています。
この動画の要点
- 従来のRedisの数量カウンターモデルは同時実行を処理しましたが、予約のクリーンアップとMySQL元帳の更新は単一のローカルアトミックトランザクションを共有できませんでした。
- 代替システムでは、アイテム/ロケーションの組み合わせごとに1,000個に制限されたプールに、利用可能なユニットごとに1行を使用します。空のプールは、補充ロックの背後で同時リクエストが待機している状態でインライン補充をトリガーできます。
- 複合主キーは(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に移行し、ユニットごとに1行を バウンドプールに格納しました。なぜMySQLを選んだのでしょうか? 安全にロックをスキップするにはどうすればよいですか? これはThe Daily Diff、舞台裏です。 そして、なぜ高速なクエリがまだ上限に達したのでしょうか? Shopifyはオンラインと実店舗での販売のためのコマースプラットフォームです。 Redisはインメモリデータストアです。
0:21 MySQLはリレーショナルデータベースであり、Shopifyは既に在庫元帳を そこに保管していました。予約は購入者が支払う間、一時的に在庫を保持します。
2つのストアが危険だった理由
0:29 彼らのRedisモデルは、アイテムカウンターをデクリメントしました。 支払い済みの注文を請求するということは、MySQL元帳を更新し、Redisをクリーンアップすることを意味しました。 これらの別々の書き込みにより、在庫が二重に販売されたり、販売可能であるべきときに利用できなくなったりする可能性がありました。 古いモデルにはロケーション認識も欠けていました。 代替システムは、注文を満たすことができる場所から在庫を選択する必要があります。 間違った大陸の倉庫は、優れたデータベースエントリであり、 ひどい配達約束です。 以前のMySQLの試みでは数量行を使用していたため、競合するチェックアウトは
ロック可能な単位は何になるのか
0:56 同じロックでキューに入れられました。スプレッドシートのセルを囲むベルベットのロープを想像してください。 ワーカーを増やしても、キューが長くなるだけです。 Shopifyはロック可能なオブジェクトを変更しました。 利用可能な各ユニットには独自の行が割り当てられます。 ロック読み取りは、別の トランザクションが保持しているユニットをスキップし、他の利用可能な ユニットを選択します。異なるワーカーが異なる行を取得でき、 ホットカウンターは邪魔になりません。
プールが空になったらどうなるか
1:15 そのプールは、アイテムと場所ごとに1,000行に制限されています。 補充は元帳から引き出されます。 空になると、予約 パスはインラインで補充され、競合するリクエストは 補充ロックの背後で待機します。 空のプールは空の倉庫を意味しません。 予約は選択されたプール行を削除し、トランザクションで予約レコードを挿入します。 コミットはデータベースロックを解放します。
1:35 ロールバックは変更を元に戻します。 予約は支払い処理中に保存された状態として存続します。 支払い成功は元帳を請求し、予約をアトミックに削除します。 データベースロックは支払いフォームを見守る必要はありません。 彼らの複合主キーはショップ、アイテム、 グループ、次にユニットIDで始まります。 ルックアップを一致させることで、プロトタイプでのインデックスロックが削減されました。 また、プール補充をブロックするギャップロックを避けるために読み取りコミットを使用し、
1:57 循環待機を防ぐために一貫したテーブル順序を使用しています。 公開されている例には有効期限が記録されています。 放棄された支払いは最終的に在庫が解放される必要があり、さもないとショッピングカートが 大家になってしまいます。Shopifyの投稿では、そのクリーンアップアルゴリズムが不明確であるため、 この図はライフサイクル要件を示しています。 ここに落とし穴があります。
SKIP LOCKEDは何を残しているか
2:14 スキップロックはロックされた行を除外するため、マニュアルはその結果を一貫性のない ビューと呼んでいます。完全な在庫数も公平な順番付けも提供しません。 可用性の決定と補充ルールをその周りに置いてください。
本当の天井はどこにあったのか
2:26 そしてその天井は? 他のチェックアウトコードがデータベース接続を長時間保持していました。 Shopifyは呼び出し元にタグを付け、接続保持時間を測定し、 その後、チェックアウトパスをクリーンアップし、スレッドの同時実行性を見直しました。 高速なクエリでもクエリの外でキューに入れることができます。
なぜこの設計を出荷するのか
2:38 彼らはRedisを権威として両方のシステムをシャドウ書き込みし、 結果を比較し、キルスイッチで徐々に切り替えました。 私の判断は「SHIP IT」です。 共有トランザクション境界とロールバック可能な展開を チェックアウトパス全体に計測を施して出荷します。 これについて質問がありますか? コメントに入れてください。 今日のThe Daily Diffは以上です。
2:54 AxrisiのNikoです。 責任を持ってマージしてください。
情報源
- 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



