在 redis 裡,所謂 setnx,是「set if not exists」的縮寫,也就是只有不存在的時候才設定,可以利用它來實現鎖的效果,不過很多人沒有意識到 setnx 有陷阱!
比如說:某個查詢資料庫的介面,因為呼叫量比較大,所以加了快取,並設定快取過期後重新整理,問題是當併發量比較大的時候,如果沒有鎖機制,那麼快取過期的瞬間,大量併發請求會穿透快取直接查詢資料庫,造成雪崩效應,如果有鎖機制,那麼就可以控制只有乙個請求去更新快取,其它的請求視情況要麼等待,要麼使用過期的快取。
下面以目前 php 社群裡最流行的 phpredis 擴充套件為例,實現一段演示**:
<?php
$ok = $redis->setnx($key, $value);
if ($ok)
?>
快取過期時,通過 setnx 獲取鎖,如果成功了,那麼更新快取,然後刪除鎖。看上去邏輯非常簡單,可惜有問題:如果請求執行因為某些原因意外退出了,導致建立了鎖但是沒有刪除鎖,那麼這個鎖將一直存在,以至於以後快取再也得不到更新。於是乎我們需要給鎖加乙個過期時間以防不測:
<?php
$redis
->multi();
$redis
->setnx($key, $value);
$redis
->expire($key, $ttl);
$redis
->exec();
?>
因為 setnx 不具備設定過期時間的功能,所以我們需要借助 expire 來設定,同時我們需要把兩者用 multi/exec 包裹起來以確保請求的原子性,以免 setnx 成功了 expire 卻失敗了。 可惜還有問題:當多個請求到達時,雖然只有乙個請求的 setnx 可以成功,但是任何乙個請求的 expire 卻都可以成功,如此就意味著即便獲取不到鎖,也可以重新整理過期時間,如果請求比較密集的話,那麼過期時間會一直被重新整理,導致鎖一直有效。於是乎我們需要在保證原子性的同時,有條件的執行 expire,接著便有了如下 lua **:
local key = keys[1]
local
value = keys[2]
local ttl = keys[3]
local ok = redis.call('setnx', key, value)
if ok == 1 then
redis.call('expire', key, ttl)
endreturn ok
沒想到實現乙個看起來很簡單的功能還要用到 lua 指令碼,著實有些麻煩。其實 redis 已經考慮到了大家的疾苦,從 2.6.12 起,set 涵蓋了 setex 的功能,並且 set 本身已經包含了設定過期時間的功能,也就是說,我們前面需要的功能只用 set 就可以實現。
<?php
$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($ok)
?>
如上**是完美的嗎?答案是還差一點!設想一下,如果乙個請求更新快取的時間比較長,甚至比鎖的有效期還要長,導致在快取更新過程中,鎖就失效了,此時另乙個請求會獲取鎖,但前乙個請求在快取更新完畢的時候,如果不加以判斷直接刪除鎖,就會出現誤刪除其它請求建立的鎖的情況,所以我們在建立鎖的時候需要引入乙個隨機值:
<?php
$ok=
$redis
->
set($key, $random, array('nx', 'ex'
=>
$ttl));
if ($ok)
}?>
redis分布式鎖
redis分布式鎖 直接上 我寫了四個redis分布式鎖的方法,大家可以提個意見 第一種方法 redis分布式鎖 param timeout public void lock long timeout thread.sleep 100 catch exception e override publi...
Redis分布式鎖
分布式鎖一般有三種實現方式 1.資料庫樂觀鎖 2.基於redis的分布式鎖 3.基於zookeeper的分布式鎖.首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件 互斥性。在任意時刻,只有乙個客戶端能持有鎖。不會發生死鎖。即使有乙個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保...
redis分布式鎖
使用redis的setnx命令實現分布式鎖 redis為單程序單執行緒模式,採用佇列模式將併發訪問變成序列訪問,且多個客戶端對redis的連線並不存在競爭關係。redis的setnx命令可以方便的實現分布式鎖。setnx key value 將key的值設為value,當且僅當key不存在。如給定的...