Redis 分散ロック
分散システムに触れ始めたばかりの頃、多くの人がある問題に突き当たります。
複数のサービスインスタンスが同時に同じ処理を行うとき、データの二重処理をどう防ぐか?
たとえば次のようなケースです。
- ユーザーによるクーポンの争奪
- 定期タスクの実行
- 在庫の引き当て
- 注文ステータスの更新
システムにプロセスが 1 つしかなければ話は簡単で、ローカルロック(mutex) で解決できます。しかしマイクロサービスアーキテクチャやクラスタ構成になると、問題は変わります。システムには 10 個のサービスインスタンス、100 個の Worker、さらには複数のデータセンターが存在するかもしれません。こうなるとローカルロックは完全に無力です。異なるプロセス同士は、お互いのロック状態をまったく知り得ないからです。
そこで登場したのが 分散ロック(Distributed Lock) という概念です。その目標はシンプルです。
分散環境において、ある瞬間に 1 つのノードだけがあるロジックを実行できることを保証する。
なぜ Redis で分散ロックを実現できるのか?
分散ロックを実装しようとするとき、多くの人が最初に思いつくのはデータベースです。たとえば:
-- データベースの行ロックを利用して相互排他を実現する
select ... for update
しかしデータベースロックの問題は、性能が悪く、ロックの粒度が大きく、高並行時に負荷が大きいことです。そこで、ロックにより適したシステムが求められるようになり、Redis が非常に適任でした。Redis には生まれつき 3 つの強みがあるからです。
1)シングルスレッドモデル
Redis のコアコマンドはシングルスレッドで実行され、同時に実行されるコマンドは常に 1 つだけです。そのため、次のような操作は:
# NX: key が存在しないときだけ書き込む。天然の相互排他
SET key value NX
絶対的なアトミック操作 であり、レースコンディションは発生しません。
2)メモリ操作で、非常に高速
Redis のデータはすべてメモリ上にあり、1 回のロック操作は通常 SET が 1 回、DEL が 1 回で済みます。遅延は通常 数十マイクロ秒のレベル で、データベースよりはるかに高速です。
3)自動的な有効期限をサポート
Redis の key には TTL を設定できます。たとえば:
# NX: 存在しないときだけ作成。EX 10: 10 秒で自動的に期限切れ
SET lock:order 123 NX EX 10
こうしておけば、たとえサービスがクラッシュしても、ロックが永遠に残り続けることはありません。
Redis ロックの解放
ロックを解放するとき、直接 DEL を実行してはいけません。次のような状況が起こり得るからです。
スレッド A がロックを取得
スレッド A の実行がタイムアウト
ロックが期限切れ
スレッド B がロックを取得
スレッド A が実行を終えてロックを削除
このとき、スレッド A はスレッド B のロックを誤って削除してしまいます。正しいやり方は、自分が保持しているロックだけを削除する ことで、通常は Lua スクリプトで実現します。
-- まずロックの値(一意な識別子)を照合し、一致した場合のみ削除して、自分のロックだけを解放することを保証する
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
Lua スクリプトは Redis の中で全体としてアトミックに実行され、GET と DEL の間に他のコマンドが割り込むことはありません。これによって初めて安全な解放が保証されます。
Redis 分散ロックの古典的な問題
Redis のロックはシンプルですが、実際のシステムでは多くの落とし穴に遭遇します。最もよくあるのは次の 2 つです。
1)ロックの期限切れ問題
ビジネスの実行時間が TTL を超えると、ロックは先に解放されてしまい、他のノードが同時に処理へ進んでしまう可能性があります。解決策は 自動延長(WatchDog) です。たとえば:
- ロックの TTL を 10 秒に設定する
- 残り有効期間が最後の 30% に入ったら自動的に 1 回延長し、さらにランダムなジッターを加えて、大量の延長リクエストが同じ瞬間に Redis へ集中するのを避ける
タスクが実行中である限り、ロックが期限切れになることはありません。
2)Redis の単一障害点問題
Redis がダウンすると、すべてのロックが無効になります。よくある解決策は次のとおりです。
- Redis Sentinel
- Redis Cluster
- RedLock アルゴリズム
Redis の AP 設計と etcd・ZooKeeper の CP アーキテクチャの違い
Redis 自体は AP(可用性優先) 寄りのシステムです。ネットワーク分断やノード障害が起きたとき、Redis はデータの絶対的な一貫性よりもサービスの可用性を優先します。これが 1 つのリスクをもたらす可能性があります——極端なケースでは、2 つのクライアントが同時に「自分がロックを取った」と考える状況が起こり得る のです。
たとえば:
クライアント A がマスターノードでロックを取得した
マスターノードがスレーブノードへ同期する前に
マスターノードが突然ダウンした
スレーブノードが新しいマスターノードに昇格した
このとき、新しいマスターノードは A がすでにロックを取得していたことを知らないため、クライアント B が再びロックを取得でき、2 つのクライアントが同時にロックを保持する状況が発生します。これが Redis 分散ロックの理論上の一貫性リスクです。
これに対して、etcd や ZooKeeper のようなシステムは CP モデル(強一貫性) に基づいて設計されており、内部で Raft や ZAB プロトコル によって過半数ノードの確認を経て初めてデータをコミットします。そのためロックの取得が成功すれば、クラスタ全体で合意が形成され、複数のクライアントが同時にロックを取得する状況は発生しません。
そこでエンジニアリングの実践では、一般に次のような経験則があります。ビジネスレベルのロック(在庫、タスク制御など) には Redis 分散ロックで通常十分ですが、強一貫性の要求が非常に高いシーン——金融取引、グローバルスケジューリングシステム、分散協調サービス——では、多くのシステムが etcd / ZooKeeper のような強一貫性のロック実装を選択します。
まとめ
Redis 分散ロックの核心となる考え方は非常にシンプルです。Redis のアトミック操作を利用して相互排他を実現する こと。そこには 3 つの重要なポイントがあります。アトミックなロック取得、安全な解放、そして自動的な期限切れまたは延長です。実際のエンジニアリングでは、成熟した Redis ロックには通常さらにリトライ機構、WatchDog による自動延長、Lua による安全な解放、ロックの一意な識別子が加わります。同時に、Redis が AP 寄りであるという位置づけも理解しておくべきです——ビジネスレベルの相互排他には十分ですが、強一貫性が必要なシーンでは etcd / ZooKeeper を検討しましょう。これらの設計が組み合わさって初めて、本当に安定した信頼できる分散ロックシステムになるのです。
COMMENTS