分散ロックの要件
分散システムで共有データを操作する際、複数プロセス間の調整が必要です。高可用性のある分散ロックは以下の特性を満たす必要があります:
- 同一メソッドが同時に単一ノードの単一スレッドで実行される
- ロック取得・解放の高パフォーマンス
- デッドロック防止のための有効期限機構
- 再入可能なロック取得
- 非ブロッキングな取得失敗時の動作
Redisスタンドアロン実装
Redisの単一スレッド特性を活用した実装手法:
基本実装と進化
基本方式: SETNXコマンドでロック取得後、DELで解放。問題点:クライアント障害時のデッドロック。
有効期限追加: SETNX後にEXPIREを実行。問題点:非原子操作による有効期限設定失敗の可能性。
原子操作化: 拡張SETコマンドでロック取得と有効期限設定を原子化:
SET resource_key unique_value PX 5000 NX
問題点:他クライアントのロック誤解放。
最終解決策
Luaスクリプトによる原子操作で値検証と削除を実現:
String lua =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object res = redis.eval(lua, List.of(lockName), List.of(clientToken));
この実装でスタンドアロン環境の要件を満たします。
Redisクラスタ環境実装
マスター障害時のフェイルオーバーで発生するロック競合を解決するため、RedLockアルゴリズムを採用:
RedLockアルゴリズム
- 現在のUnixタイムスタンプ取得
- N個のRedisノードで順次ロック取得(単一ノード方式と同様)
- 以下の条件を満たす場合にロック成功:
- ロック有効時間 = (現在時刻 - 要求時刻) < ロックTTL
- 過半数ノードで取得成功
- 実効有効時間 = ロックTTL - 取得処理時間
- 全ノードに解放コマンドを送信
Redisson実装
RedissonはRedLockを実装した公式推奨ライブラリ:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
設定例:
// クラスタ設定
Config cfg = new Config();
cfg.useClusterServers()
.addNodeAddress("redis://node1", "redis://node2");
RedissonClient client = Redisson.create(cfg);
RLock lock = client.getLock("shared_lock");
try {
if(lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// クリティカルセクション処理
}
} finally {
lock.unlock();
}
内部実装ではハッシュ構造で再入可能ロックを管理:
// ロック取得Luaスクリプト
String lockScript =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('hset', KEYS[1], ARGV[2], 1); " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
"if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
" redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);";