redis-py の Lock クラスは UUID ベースのトークンでロックの所有権を管理するが、フェンシングトークン(単調増加する数値)は提供しない。しかし、Lock クラスは do_acquire や Lua スクリプトをオーバーライドできる設計になっており、サブクラス化でフェンシングトークンを追加できる。
本記事では、redis-py の Lock を拡張してフェンシングトークンを発行する FencedLock クラスの実装例を紹介する。
前提知識:Redis の Lua スクリプティング
Redis はバージョン 2.6 から Lua スクリプトの実行機能を内蔵している。EVAL コマンドで Lua スクリプトを Redis サーバー上で直接実行でき、複数の Redis コマンドをアトミック(不可分)に実行できる。
なぜ Lua スクリプトが必要か
通常、Redis コマンドは1つずつ実行される。例えば「キーが存在しなければセットし、同時にカウンターをインクリメントする」という処理を2つのコマンドで行うと、その間に他のクライアントが割り込む可能性がある:
クライアント A: SET mykey value NX → 成功
← クライアント B が割り込む余地
クライアント A: INCR counter → インクリメント
Lua スクリプトを使えば、この2つの操作を1回のアトミックな呼び出しにまとめられる:
| |
Redis CLI での実行例
| |
redis-py での実行例
| |
セキュリティ上の注意
Lua スクリプトのパラメータは KEYS[] と ARGV[] で渡される。SQL のプリペアドステートメントと同様に、パラメータが文字列としてスクリプトに展開されることはないため、パラメータ経由でのインジェクションはできない。ただし、ユーザー入力でスクリプト文字列自体を動的に組み立てると危険なので、スクリプトは固定文字列として定義すること。
また、Redis は Lua 環境で loadstring(), dofile(), os, io ライブラリを無効化しており、OS コマンド実行やファイルアクセスはできない。
redis-py の Lock のカスタマイズポイント
redis-py の Lock クラスは、以下のメソッドをオーバーライドすることでカスタマイズできる:
| メソッド | 役割 |
|---|---|
do_acquire(token) | 実際のロック取得処理(SET NX PX) |
do_release(expected_token) | Lua スクリプトによるロック解放 |
do_extend(additional_time, replace_ttl) | TTL の延長 |
通常の do_acquire は UUID トークンを SET key <uuid> NX PX <timeout> で書き込むだけだ。ここにフェンシングトークンの発行を追加する。
FencedLock の実装
| |
実装のポイント
- Lua スクリプトでアトミック化 —
SET NXとINCRを1回の EVALSHA で実行する。2つのコマンドを別々に発行すると、ロック取得とトークン発行の間に他のクライアントが割り込む可能性がある - フェンスキーに TTL を設定しない —
fence:{name}は単調増加を保つために永続化する。TTL を設定すると、キーが消えた時点でカウンターがリセットされ、フェンシングの安全性が崩れる - 既存の Lock API との互換性 —
do_acquireをオーバーライドするだけなので、blocking,blocking_timeout,thread_localなどの既存オプションはすべてそのまま使える
使い方
基本的な使用例
| |
コンテキストマネージャとして使用
| |
redis-py の Lock.__enter__ は self を返すため、with ... as lock でそのままフェンシングトークンにアクセスできる。
Django(django-redis)との統合
django-redis の cache.lock() は内部で redis-py の Lock をそのまま使うため、FencedLock を直接差し込むことはできない。代わりに、redis-py のクライアントを直接取得して使う:
| |
モデル側の準備
フェンシングトークンを検証するには、対象テーブルに fence_token カラムが必要だ:
| |
注意点と限界
フェンスキーの永続化
fence:{name} キーは Redis の永続化設定(RDB スナップショットや AOF)に依存する。Redis が再起動してフェンスキーが失われた場合、カウンターは 0 からリスタートする。これが許容できない場合は、フェンシングトークンの発行をデータベース側で行う方が安全だ:
| |
Redlock との併用
本実装はシングルインスタンスの Redis を前提としている。Redlock(複数 Redis インスタンスによる合意ベースのロック)と併用する場合、各インスタンスの INCR が異なる値を返すため、単調増加が保証されない。Redlock 環境でフェンシングトークンが必要な場合は、トークン発行を Redis 以外の仕組み(データベースや ZooKeeper)に委ねる必要がある。
まとめ
redis-py の Lock は do_acquire のオーバーライドでフェンシングトークンを追加できる設計になっている。Lua スクリプトでロック取得とトークン発行をアトミックに行うことで、既存の Lock API との互換性を保ちつつ、データストア側での整合性検証が可能になる。
ただし、フェンシングトークンはあくまで「データストア側での最終防衛線」であり、ロック自体の信頼性を高めるものではない。ロックの TTL 設定、処理時間の見積もり、冪等な設計といった基本的な設計原則と組み合わせて使うことが重要だ。
参考
- redis-py Lock ソースコード — redis-py の Lock クラスの実装
- How to do distributed locking — Martin Kleppmann によるフェンシングトークンの提唱
- Distributed Locks with Redis — Redis 公式の分散ロックパターン
- fencelock — Redis モジュールとしてのフェンシングトークン実装(C 言語)