Redisベースのlocking実装
3806 ワード
Redisベースのlockは,その単一プロセス単一スレッドとその原子操作に基づいて実現される.Redisの場合、同じ時点で1つのコマンドが動作している可能性があります.つまり、Redisのレベルでは、リクエストはシリアルで行われます.
SETNX
SETNXはRedisのコマンドであり、完全な形式は次のとおりです.
これは「set if not exists」の略語であり、その説明のようにSETNXの役割はkeyに値を付与し、ターゲットkeyが存在しない場合にのみ成功して1を返す.
ロック実装は主にRedisに基づくこのコマンドである
プロセスは次のとおりです.まずClientAはロックを取得する必要があり、その後SETNX lock_name time_stampは1を返し、ロックは に成功した. ClientBはこのロックを取得しようとしたが、SETNXを試みた結果、keyが既に存在するため、ロックが占有されていることを知り、待機または に戻る. ClientAでやるべきことが終わったら、DELコマンドでlock_を削除します.名前は、ロックを解除する動作 を完了するために使用される.この時点で、このロックは他のClientによって占有することができる .
完璧に見えますが、実はこの中には細部の問題がたくさん存在しています.大まかに分析してみましょう. ClientAのSETNXとDELは別々の操作であるため、ClientAがロックをかけた後に何らかの理由でこのロックを解放しなかった場合、深刻な結果をもたらすという問題がある .上の問題は実際に設計時にすでに考えられており、解決策はこのロックに期限切れ時間を加えることであり、ロックが期限切れ時間を超えた後、他のClientはそれを解放し、SETNXを通じて を占領することができる.しかし、上記の解決方法には別の問題があります.それは、このような状況が存在する場合です. ClientAはロックをかけた後にロックを解除していない.この時ClientBとClientCは を待っている.ロックがオーバーすると、BとCは同時にタイムアウトを検出し、DEL操作を実行し、SETNXロック を実行する.その単一の動作はすべて原子性である、Cが先にDELした後にSETNXがロックを奪った時、BはDELを実行してまたロックを解放し、最後の2つはロック を獲得した.
では、ClientAが死んでいない場合、非常に時間のかかる操作を実行し、ロックが期限切れになっても実行されず、他の人がロックを奪った後、DELロックを解除する操作が完了した場合もあります.GG
次はredis-objectsというgemがどのようにこのlockingを実現したのかを見てみましょう.
Locking
ソースコードの実装を見るのは簡単で、全部で数十行のコードです.
まずロックを取得するプロセスを見てみましょう.まず、タイムアウト時間内にロックを取得しようとします.
タイムアウト時間内に0.1秒おきに試行することがわかります
ロックメソッドに戻り、ロックを取得しようとする手順は次のとおりです. SETNXコマンドを使用してロックを取得しようとしたが、ループを正常に飛び出して真の論理 を実行すると失敗した場合はロックの有効期限をチェックし、有効期限が切れていなければ次のラウンドに入る試み を待つ.ロックが期限切れであることを確認した場合、GETSETコマンドを使用してlock_keyは値を付与し、元の値を返して、期限が切れたかどうかを見て、次の試み を待っていません.期限が切れたら鍵を手に入れて、自分のことを始めることができます 自己の完了後、ロックを解除する操作の前に、自己がロックを取得したときに定める期限切れが経過しているか否かをチェックし、タイムアウトしていればロックを解除しない操作 を行う.
SETNX
SETNXはRedisのコマンドであり、完全な形式は次のとおりです.
SETNX key value
これは「set if not exists」の略語であり、その説明のようにSETNXの役割はkeyに値を付与し、ターゲットkeyが存在しない場合にのみ成功して1を返す.
ロック実装は主にRedisに基づくこのコマンドである
プロセスは次のとおりです.
完璧に見えますが、実はこの中には細部の問題がたくさん存在しています.大まかに分析してみましょう.
次はredis-objectsというgemがどのようにこのlockingを実現したのかを見てみましょう.
Locking
ソースコードの実装を見るのは簡単で、全部で数十行のコードです.
# Get the lock and execute the code block. Any other code that needs the lock
# (on any server) will spin waiting for the lock up to the :timeout
# that was specified when the lock was defined.
def lock(&block)
expiration = nil
try_until_timeout do
expiration = generate_expiration
# Use the expiration as the value of the lock.
break if redis.setnx(key, expiration)
# Lock is being held. Now check to see if it's expired (if we're using
# lock expiration).
# See "Handling Deadlocks" section on http://redis.io/commands/setnx
if !@options[:expiration].nil?
old_expiration = redis.get(key).to_f
if old_expiration < Time.now.to_f
# If it's expired, use GETSET to update it.
expiration = generate_expiration
old_expiration = redis.getset(key, expiration).to_f
# Since GETSET returns the old value of the lock, if the old expiration
# is still in the past, we know no one else has expired the locked
# and we now have it.
break if old_expiration < Time.now.to_f
end
end
end
begin
yield
ensure
# We need to be careful when cleaning up the lock key. If we took a really long
# time for some reason, and the lock expired, someone else may have it, and
# it's not safe for us to remove it. Check how much time has passed since we
# wrote the lock key and only delete it if it hasn't expired (or we're not using
# lock expiration)
if @options[:expiration].nil? || expiration > Time.now.to_f
redis.del(key)
end
end
end
まずロックを取得するプロセスを見てみましょう.まず、タイムアウト時間内にロックを取得しようとします.
def try_until_timeout
if @options[:timeout] == 0
yield
else
start = Time.now
while Time.now - start < @options[:timeout]
yield
sleep 0.1
end
end
raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec"
end
タイムアウト時間内に0.1秒おきに試行することがわかります
ロックメソッドに戻り、ロックを取得しようとする手順は次のとおりです.