setNX,是set if not exists 的縮寫,也就是隻有不存在的時候才設置, 設置成功時返回 1 , 設置失敗時返回 0 。能夠利用它來實現鎖的效果,可是不少人在使用的過程當中都有一些問題沒有考慮到。
例如某個查詢數據庫的接口由於請求量比較大因此加了緩存,並設定緩存過時後刷新。當併發量比較大而且緩存過時的瞬間,大量併發請求會直接查詢數據庫致使雪崩。若是使用鎖機制來控制只有一個請求去更新緩存就能避免雪崩的問題。下面是不少人下意識想到的加鎖方法php
$rs = $redis->setNX($key, $value); if ($rs) { //處理更新緩存邏輯 // ...... //刪除鎖 $redis->del($key); }
經過 setNX 獲取鎖,若是成功了則更新緩存而後刪除鎖。其實這裏有一個嚴重的問題:若是更新緩存的時候由於某些緣由意外退出了,那麼這個鎖就不會被刪除而一直存在,以致於緩存再也得不到更新。爲了解決這個問題有人可能會想到給鎖設置一個過時時間,以下redis
$redis->multi(); $redis->setNX($key, $value); $redis->expire($key, $ttl); $redis->exec();
由於 setNX 不具有設置過時時間的功能,因此要藉助 Expire 來設置,同時須要使用 Multi/Exec 來確保請求的原子性,以避免 setNX 成功了 Expire 卻失敗了。這樣還有問題:當多個請求到達時,雖然只有一個請求的 setNX 能夠成功,可是任何一個請求的 Expire 卻均可以成功,這就意味着即使獲取不到鎖也能夠刷新過時時間,致使鎖一直有效,仍是解決不了上面的問題。顯然 setNX 知足不了需求,Redis從 2.6.12 起,SET 涵蓋了 SETEX 的功能, SET 自己又包含了設置過時時間的功能,因此使用 SET 就能夠解決上面遇到的問題數據庫
$rs = $redis->set($key, $value, array('nx', 'ex' => $ttl)); if ($rs) { //處理更新緩存邏輯 // ...... //刪除鎖 $redis->del($key); }
到這一步其實仍是有問題的,若是一個請求更新緩存的時間比鎖的有效期還要長,致使在緩存更新過程當中鎖就失效了,此時另外一個請求就會獲取到鎖,但前一個請求在緩存更新完畢的時候,直接刪除鎖的話就會出現誤刪其它請求建立的鎖的狀況。因此要避免這種問題,能夠在建立鎖的時候須要引入一個隨機值並在刪除鎖的時候加以判斷緩存
$rs = $redis->set($key, $random, array('nx', 'ex' => $ttl)); if ($rs) { //處理更新緩存邏輯 // ...... //先判斷隨機數,是同一個則刪除鎖 if ($redis->get($key) == $random) { $redis->del($key); } }