この記事は以下の記事の続きです。先に前の記事を読むことをお勧めします。
今回は前に投稿した上記記事の最後で指摘したロックがタイムアウトした場合に起こる問題を解消します。
1.仕組み
簡易版での問題はロックのタイムアウト時間より処理が長くなってしまった場合、その長くなってしまった処理が最後に自分のロックしたものでないキーを削除してしまうことで意図せずロックが解除されてしまうということでした。
なのでロックを解除する際に自分のかどうかを調べて、自分の獲得したロック出なかった場合は削除しないようにすれば解決します。
1.1.NG例
実は既に前回のプログラムでも自分の獲得したロックかどうかの値はUUIDとしてロックキーに対応する値として入れてあります。
なので、ロックをリリースしている箇所net.penguinsan.redis.MutexRedisServiceのreleaseMutex
メソッドで以下のようにしてやればよいでしょうか?
public void releaseMutex(Mutex m) {
final String objectKey = m.getObjectKey();
try (Jedis r = this.jedisPool.getResource()) {
String lockId = r.get(objectKey);
if (lockId.equals(m.getLockId())) {
r.del(objectKey);
System.out.printf("delete key : lock_id=%s\n", m.getLockId());
} else {
System.out.printf("not delete key : lock_id expect=%s actual=%s\n", m.getLockId(), lockId);
}
}
}
これだと最初のgetから次のdelまでの間にRedis側で値が書き換わってしまう可能性がある(間にRedisのコマンドが割り込む可能性がある)ので確実ではありません(大抵はうまくいってしまうかもしれませんが・・・)
1.2.正解例
getとdelの間にコマンドを割り込ませず判定するにはどうするか?
Luaスクリプトで上記と同じ判定を記述してRedis側で実行すれば間にコマンドを挟まれることなく実行できます。
public void releaseMutex(Mutex m) {
try (Jedis r = this.jedisPool.getResource()) {
r.eval(
"local id = redis.call('GET',KEYS[1])\n"
+ "if id == ARGV[1] then\n"
+ " redis.call('DEL',KEYS[1])\n"
+ "end\n",
List.of(m.getObjectKey()), List.of(m.getLockId()));
}
}
Luaスクリプトに関しては詳しく説明しませんが、RedisにevalコマンドでLuaスクリプトと必要なパラメータを渡してやることでスクリプトに書かれた一連の処理をアトミックに実行することができます。
2.まとめ
上記のコードをもう少しまとめて書き直したものをgithubに公開してありますので参考にしてみてください。
Luaスクリプトを駆使すると分散処理系で各サーバーにユニークなIDを発行したり、いろいろなことができるので面白いです。