Redis分散ロック徹底解説:実証済みパターン、よくある落とし穴、実践的な活用法

はじめに
分散ロックは、本番環境で実際に依存するようになるまで、非常にシンプルなものに聞こえます。
あるプロセスがリソースへの排他的アクセスを必要としている。複数のサーバーが稼働している。Redisがその中間に位置する。「Redisにロックを置けばあとは進むだけ」という発想は、一見まっすぐで理にかなっているように感じられます。
しばらくの間、このアプローチはうまく機能しているように見えます。しかし、ある日プロセスがクラッシュしたり、ネットワーク遅延が発生したり、レイテンシが急上昇したりします。突然、2つのプロセスが同じロックを取得してしまったり、どのプロセスもロックを所有していない状態になったり、ロックが永遠に解放されなくなったりするのです。
Redisのロックに関する問題のほとんどは、Redis自体が原因ではありません。分散ロックが誤解されていることが原因です。Redisは調整のためのツールを提供するものであり、完全な排他性を保証するものではありません。
分散ロックとは何か(本来の姿)
分散ロックはインメモリのミューテックスとは異なります。絶対的な保証を提供するものではなく、障害を排除することもできません。
分散ロックとは、信頼できない環境で動作する「調整メカニズム」です。ネットワークは障害を起こし、プロセスはクラッシュし、時計はずれます。完璧な動作を前提としたロック戦略は、いずれ必ず破綻します。
Redisのロックはベストエフォート型のロックです。正しく設計すれば極めて効果的ですが、安易に実装すると、微妙な競合状態やデータ破損を引き起こします。
Redisのロックを安全に使うための第一歩は、期待値を適切に調整することです。
Redisロックが有効なケース
Redisの分散ロックが適しているのは、次のようなケースです。
- クリティカルセクションの同時実行を防ぎたい場合
- 複数サーバー間で作業を調整する必要がある場合
- 処理が短時間で完了する場合
- リトライが許容できる場合
代表的なユースケースには、ジョブの重複排除、定期タスクの調整、キャッシュ再構築、二重処理の防止などがあります。
一方で、長時間実行されるビジネストランザクション、ユーザー向けの重要なワークフロー、安全にリトライできない操作には、Redisロックは不向きです。リトライが許されないのであれば、Redisロックは誤った道具選びです。
Redisにおける唯一の安全なロックプリミティブ
最も安全なRedisのロックプリミティブは、単一のアトミックコマンドです。
SET key value NX EX ttl
このコマンドは、キーがまだ存在しない場合にのみロックを作成し、一意のオーナー値を設定し、TTLを指定して自動的に期限切れになるようにします。
他のアプローチはすべて競合状態を引き起こします。TTLなしでSETNXを使うことや、別ステップで有効期限を設定することは、障害発生時に破綻します。TTLは必須であり、プロセスが予期せずクラッシュした際にシステムを保護します。
ロックTTLが必須である理由
TTLがないと、Redisのロックは永遠に生き続ける可能性があります。プロセスがロックを保持したままクラッシュすれば、システムは静かに停止してしまいます。
TTLによってロックは自己修復的になります。障害が発生しても、ロックはいずれ期限切れとなり、処理を再開できるようになります。その結果、短時間の重複が生じる可能性がありますが、これは意図的なトレードオフです。
決して期限切れにならないロックは、ロックがないことよりも危険です。
ロックの所有権と安全な解放
Redisのロックは、それを取得したプロセスだけが解放すべきです。だからこそ、ロックの値には定数文字列ではなく、一意の識別子(UUIDなど)を使用すべきなのです。
ロックを解放するには、所有権の検証が必要です。安全な方法はLuaスクリプトを使い、保存された値を確認し、期待されるオーナーと一致する場合にのみロックを削除することです。
これにより、「ロックが期限切れになり、別のプロセスが取得した後、元の保持者が誤って削除してしまう」という競合状態を防げます。
安全なロック時間の選び方
ロックのTTLは、保護対象の操作の最大想定実行時間を超えている必要があります。平均実行時間では不十分です。
作業がまだ実行中なのにロックが期限切れになると、別のプロセスがそのロックを取得し、同じ作業を並行して実行してしまう可能性があります。これにより、重複や破損が発生しかねません。
一方で、過度に長いTTLは、問題が発生したときの復旧を遅らせます。ロック時間はシステムの変化に応じて調整すべきバランスの問題です。
ロック下の作業を小さく、範囲を限定して保つのが最も安全なアプローチです。
ロックの延長とハートビート
想定より時間のかかる操作もあります。TTLを定期的に更新してロックを延長することは可能ですが、複雑さと新たな障害モードが加わります。
TTLを延長できるのはロックのオーナーのみであるべきで、所有権を失った時点で延長は即座に停止しなければなりません。
多くの場合、ロック延長ロジックを実装するよりも、作業をより小さなチャンクに分割するよう再設計する方が安全です。
Redlock論争
Redlockは、複数のRedisインスタンスを使用してロックの安全性を高めるアルゴリズムです。しかし同時に、議論の対象であり、運用上も複雑です。
ほとんどのシステムにとって、Redlockはメリットよりも複雑さをもたらします。現実の環境では保証が難しいタイミングに関する前提に依存しています。
Redlockレベルの保証が必要なのであれば、そもそもRedisは適切な道具ではないかもしれません。トランザクショナルなロックを持つデータベースや、ZooKeeper・etcdのような専門の調整システムの方が適しています。
ほとんどのRedisユースケースでは、適切なTTLベースのロックを使う単一のRedisインスタンスで十分です。
ロックはトランザクションではない
Redisのロックは操作をトランザクション化するわけではありません。自動ロールバックを提供せず、操作の途中で障害が起きても整合性を保証しません。
ロックは並行性を減らすだけで、障害をなくすものではありません。クリティカルセクションは冪等(idempotent)に設計し、繰り返し実行されても害が生じないようにすべきです。
この設計原則は、ロック関連のバグのリスクを大幅に減らします。
Redisがダウンしたら何が起こるか
Redisがダウンまたは再起動すると、すべてのロックは消滅します。これは想定内の挙動です。
Redisが復旧すると、複数のプロセスが同時にロックを取得し、作業を開始する可能性があります。システムはこの事態に耐えられる必要があります。
Redisの再起動によるロック喪失がデータ破損につながるなら、そのロック設計は安全ではなく、見直しが必要です。
本番環境でのロック監視
Redisのロックは決して不可視であってはなりません。チームは、存在するロックの数、寿命、ロック取得失敗の頻度を監視すべきです。
異常に長く生存するロックは、停滞したプロセスを示唆します。頻繁な取得失敗は、競合やロック設計の誤りを示唆します。
可視性こそが、安全な運用に不可欠です。
よくあるRedisロックのアンチパターン
よくある間違いとしては、TTLなしでSETNXを使うこと、ロックの所有権に定数値を使うこと、ロックを無条件に削除すること、長時間実行タスクでロックを保持し続けること、そしてロックが正しさを保証すると思い込むことなどが挙げられます。
これらのアンチパターンは、実際の本番障害で頻繁に見られます。
実践的なロックチェックリスト
安全なRedisロック実装には以下が含まれます。
- TTL付きのロックキー
- 一意のロック所有権の値
- 解放時の所有権検証
- 短命なクリティカルセクション
- 冪等な操作
- テスト済みの障害経路
これらのいずれかが欠けているなら、ロック設計を見直すべきです。
Redisロックに対する健全なメンタルモデル
Redisのロックはエアバッグではなくシートベルトです。被害を軽減しますが、事故そのものを完全には防ぎません。
慎重に使えば、分散システムでの調整をシンプルにしてくれます。安易に使えば、微妙で根深いバグを持ち込みます。
まとめ
分散ロックが難しいのは、障害が避けられないからです。Redisは高速でシンプル、かつ実用的なロック機構を提供しますが、完璧なものではありません。
障害を前提に設計し、ロックは短く保ち、TTLを一貫して使い、操作がリトライ可能であることを確認しましょう。正しく使えば、Redisの分散ロックは本番システムにおいて信頼性の高い、効果的なツールとなります。
-
Redis ZINTERSTOREコマンド徹底解説 – ソート済みセットの積集合を求める方法
このチュートリアルでは、Redisデータストアに保存された2つ以上のソート済みセット(ZSET)に対して、ZINTERSTOREコマンドを使って積集合(共通部分)の演算を行う方法を詳しく解説します。 集合の積集合とは? 集合論において、2つ以上の集合の積集合(intersection)とは、すべての集合に共通して含まれる要素だけを集めた集合のことです。以下の例を見てみましょう。 A = {1, 2, 3, 4, 5} B = {4, 5, 6, 7, 8, 9} A と B の積集合: A ∩ B = {4, 5} このように、AとBの両方に存在する「4」と「5」だけが積集合の結果となります
-
Redis GETRANGEコマンドの使い方 – 文字列値から部分文字列を取得する方法
このチュートリアルでは、Redisデータストア内の指定したキーに保存された文字列値の部分文字列(サブストリング)を取得する方法を解説します。この操作には、RedisのGETRANGEコマンドを使用します。 GETRANGEコマンドとは GETRANGEコマンドは、start(開始位置・範囲に含まれる)とend(終了位置・範囲に含まれる)という2つのオフセットを受け取り、取得する部分文字列の開始インデックスと終了インデックスを指定します。 インデックスは0ベースで、0が最初の文字、1が2番目の文字を表します。また、負の数値を使うことで、文字列の末尾からのオフセットも指定できます。-1は最後の文字