在庫予約やキュー処理をRedis(メモリ上で高速に動くキー・バリュー型データストア)に任せている構成を運用しているインフラ担当者やバックエンドエンジニアに向けた内容です。ShopifyがEC注文の在庫予約システムでRedisをやめてMySQLに戻し、結果としてスケールしやすくなったという事例が話題になりました。この話は単なる技術トレンドの逆張りではなく、SREやインフラ設計の観点で見ると学びの多い落とし穴の話として整理できます。
何が起きるか
Shopifyは1日に数百万件の注文を処理する規模で、注文のたびに在庫予約(決済処理が終わるまで在庫を一時的に確保し、成功なら確定・失敗なら解放する処理)が発生します。この処理はブラックフライデーのような繁忙期に秒間数千リクエストの負荷がかかります。
従来はこの予約状態をRedisで管理していました。Redisはインメモリで高速、かつTTL(Time To Live、有効期限を自動で切る仕組み)が標準機能としてあるため、一見すると理にかなった選択に見えます。
ところが実際にはRedis層が障害点やデータ不整合の温床になっていました。最終的にMySQLのInnoDBエンジン(行単位でロックを扱うトランザクション対応のストレージエンジン)だけで再設計したところ、運用負荷が下がりスループットも十分に確保できたという結果になっています。
なぜ起きるか
原因を分解すると、Redisそのものの速度が問題だったわけではありません。問題は「クリティカルな状態管理をキャッシュ用途以外の場所に持たせたこと」による構造的な複雑さです。
1つ目は二重管理の問題です。正データはMySQLにあり、予約状態はRedisにあるという構成では、両者が食い違ったときにどちらが正しいかを判断するロジックが別途必要になります。
2つ目は永続性の弱さです。RedisのAOF(Append Only File)やRDB(スナップショット)による永続化は、リレーショナルデータベースのACID保証(原子性・一貫性・独立性・永続性)とは性質が異なります。プロセス障害やフェイルオーバーのタイミング次第でデータが失われる可能性が残ります。
3つ目は運用対象が2系統になることです。Redisクラスタの監視・レプリカ管理・メモリサイジングを、MySQLの運用と並行して続ける必要があり、SREチームが見るべきダッシュボードやアラートが単純に倍増します。
4つ目はコストです。予約データのために十分なメモリとレプリカを持つRedisクラスタを維持するのは、同じデータをすでにMySQLにも持っている前提だと二重投資になりがちです。
これらを踏まえると、Shopifyのケースは「Redisが遅かったから」ではなく「一貫性と運用のトレードオフがコストに見合わなくなったから」移行したと理解するのが正確です。
自分のプロジェクトが該当するか確認する方法
在庫予約に限らず、キューやロック・セッション管理でRedisをクリティカルパスに使っている場合は確認しておく価値があります。
- Redisのデータが失われた場合に、ビジネス上の不整合(二重販売・二重処理)が発生するか
- Redisと別のデータストア(MySQL・PostgreSQLなど)の間で、同じエンティティの状態を二重に持っているか
- Redisのpersistence設定(
appendonly yesかどうか、RDBのスナップショット間隔)をredis-cli CONFIG GET appendonlyや設定ファイルredis.confで確認したことがあるか - 障害訓練(フェイルオーバーテスト)でRedisクラスタを落として整合性を検証したことがあるか
- 利用しているMySQLのバージョンが8.0以上か。バージョンは
SELECT VERSION();で確認できます
このうち上の2項目に「はい」と答えられる場合、Redisは単なるキャッシュではなく「システムの真実の源」になっている可能性があります。その場合はShopifyのケースと同じ構造の落とし穴を抱えていると考えられます。
対策の手順
Redisを即座に廃止する必要はありません。まず取るべきは、リスクの棚卸しと段階的な移行判断です。
1. 依存の棚卸し: どのキーがキャッシュ用途で、どのキーが「失われたら困る」状態管理用途かを分類します。TTL付きキーの一覧はredis-cli --scanとパターンマッチで洗い出せます。
2. SLOの再定義: 在庫予約のような整合性が重要な処理には、可用性だけでなく「オーバーセルを許容しない」という正確性の観点をSLI(Service Level Indicator)に加えます。レイテンシSLOだけを見ていると、この種の不整合は指標に現れません。
3. MySQLでの代替設計を検証する: SELECT ... FOR UPDATE SKIP LOCKEDを使うと、ロック中の行をスキップして次の空いている行を処理できます。これはキューのような処理に向いた書き方で、MySQL 8.0以降とPostgreSQL 9.5以降で利用可能です。
SELECT * FROM inventory_reservations
WHERE product_id = ? AND status = 'available'
FOR UPDATE SKIP LOCKED
LIMIT 1;4. TTLの代替を用意する: MySQLには組み込みのTTLがないため、期限切れの予約を解放する定期ジョブ(数秒間隔のバッチ処理やイベントスケジューラ)を用意します。実行間隔をIaC(Terraform・Pulumiなど)でCronジョブやスケジュールタスクとして定義しておくと、環境ごとの設定漏れを防げます。
5. オブザーバビリティを揃える: 移行後はRedis側の監視項目(メモリ使用率・レプリカ遅延)をMySQL側の指標(ロック待ち時間・デッドロック発生数・スロークエリ)に置き換えます。SHOW ENGINE INNODB STATUSでロック競合の状況を確認できます。
6. 段階的なカナリア移行: 全トラフィックを一度に切り替えず、一部の商品カテゴリや一部リージョンから移行し、オーバーセルやレイテンシ悪化が起きないかをモニタリングしながら広げます。
まとめ
Redisは依然としてキャッシュや一時データの置き場として優秀な選択肢です。
問題になるのは、決済や在庫のようにミスが許されない状態管理までRedisに任せてしまう設計です。
まず自分のシステムで「Redisが消えたら困るデータ」と「消えても再構築できるデータ」を分類するところから始めてみてください。
そのうえでFOR UPDATE SKIP LOCKEDのような機能がMySQLやPostgreSQLで使えるかを確認し、必要なSLOと監視項目を見直すことが、次の障害を防ぐ具体的な一歩になります。