Redis的SETNX的使用方法

對應(yīng)給定的keys到他們相應(yīng)的values上。只要有一個key已經(jīng)存在,MSETNX一個操作都不會執(zhí)行。由于這種特性,MSETNX可以實現(xiàn)要么所有的操作都成功,要么一個都不執(zhí)行,這樣可以用來設(shè)置不同的key,來表示一個唯一的對象的不同字段。

在 Redis 里,所謂 SETNX,是「SET if Not eXists」的縮寫,也就是只有不存在的時候才設(shè)置,可以利用它來實現(xiàn)鎖的效果,不過很多人沒有意識到 SETNX 有陷阱!

比如說:某個查詢數(shù)據(jù)庫的接口,因為調(diào)用量比較大,所以加了緩存,并設(shè)定緩存過期后刷新,問題是當并發(fā)量比較大的時候,如果沒有鎖機制,那么緩存過期的瞬間,大量并發(fā)請求會穿透緩存直接查詢數(shù)據(jù)庫,造成雪崩效應(yīng),如果有鎖機制,那么就可以控制只有一個請求去更新緩存,其它的請求視情況要么等待,要么使用過期的緩存。

下面以目前 PHP 社區(qū)里最流行的 PHPRedis 擴展為例,實現(xiàn)一段演示代碼:

<?php

$ok = $redis->setNX($key, $value);

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

緩存過期時,通過 SetNX 獲取鎖,如果成功了,那么更新緩存,然后刪除鎖。看上去邏輯非常簡單,可惜有問題:如果請求執(zhí)行因為某些原因意外退出了,導(dǎo)致創(chuàng)建了鎖但是沒有刪除鎖,那么這個鎖將一直存在,以至于以后緩存再也得不到更新。于是乎我們需要給鎖加一個過期時間以防不測:

<?php

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

?>

因為 SetNX 不具備設(shè)置過期時間的功能,所以我們需要借助 Expire 來設(shè)置,同時我們需要把兩者用 Multi/Exec 包裹起來以確保請求的原子性,以免 SetNX 成功了 Expire 卻失敗了。 可惜還有問題:當多個請求到達時,雖然只有一個請求的 SetNX 可以成功,但是任何一個請求的 Expire 卻都可以成功,如此就意味著即便獲取不到鎖,也可以刷新過期時間,如果請求比較密集的話,那么過期時間會一直被刷新,導(dǎo)致鎖一直有效。于是乎我們需要在保證原子性的同時,有條件的執(zhí)行 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)
end

return ok

沒想到實現(xiàn)一個看起來很簡單的功能還要用到 Lua 腳本,著實有些麻煩。其實 Redis 已經(jīng)考慮到了大家的疾苦,從 2.6.12 起,SET 涵蓋了 SETEX 的功能,并且 SET 本身已經(jīng)包含了設(shè)置過期時間的功能,也就是說,我們前面需要的功能只用 SET 就可以實現(xiàn)。

<?php

$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

如上代碼是完美的嗎?答案是還差一點!設(shè)想一下,如果一個請求更新緩存的時間比較長,甚至比鎖的有效期還要長,導(dǎo)致在緩存更新過程中,鎖就失效了,此時另一個請求會獲取鎖,但前一個請求在緩存更新完畢的時候,如果不加以判斷直接刪除鎖,就會出現(xiàn)誤刪除其它請求創(chuàng)建的鎖的情況,所以我們在創(chuàng)建鎖的時候需要引入一個隨機值:

<?php

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();

    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}

?>

如此基本實現(xiàn)了單機鎖,假如要實現(xiàn)分布鎖,請參考:Distributed locks with Redis,這里就不深入討論了,總結(jié):避免掉入 SETNX 陷阱的最好方法就是永遠不要使用它。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容