談?wù)凴edis的SETNX

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

比如說:某個(gè)查詢數(shù)據(jù)庫的接口鹏漆,因?yàn)檎{(diào)用量比較大,所以加了緩存创泄,并設(shè)定緩存過期后刷新艺玲,問題是當(dāng)并發(fā)量比較大的時(shí)候,如果沒有鎖機(jī)制鞠抑,那么緩存過期的瞬間饭聚,大量并發(fā)請求會(huì)穿透緩存直接查詢數(shù)據(jù)庫,造成雪崩效應(yīng)搁拙,如果有鎖機(jī)制秒梳,那么就可以控制只有一個(gè)請求去更新緩存,其它的請求視情況要么等待箕速,要么使用過期的緩存酪碘。

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

<?php

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

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

?>

緩存過期時(shí)盐茎,通過 SetNX 獲取鎖兴垦,如果成功了,那么更新緩存字柠,然后刪除鎖探越。看上去邏輯非常簡單窑业,可惜有問題:如果請求執(zhí)行因?yàn)槟承┰蛞馔馔顺隽饲蔗#瑢?dǎo)致創(chuàng)建了鎖但是沒有刪除鎖,那么這個(gè)鎖將一直存在常柄,以至于以后緩存再也得不到更新鲤氢。于是乎我們需要給鎖加一個(gè)過期時(shí)間以防不測:

<?php

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

?>

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

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

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

if ($ok) {
    $cache->update();
    $redis->del($key);
}
//這里$ttl設(shè)置的時(shí)間要恰到好處,要大于請求執(zhí)行的時(shí)間熙暴。
?>

如上代碼是完美的嗎?答案是還差一點(diǎn)亚皂!設(shè)想一下国瓮,如果一個(gè)請求更新緩存的時(shí)間比較長乃摹,甚至比鎖的有效期還要長峡懈,導(dǎo)致在緩存更新過程中肪康,鎖就失效了,此時(shí)另一個(gè)請求會(huì)獲取鎖谒撼,但前一個(gè)請求在緩存更新完畢的時(shí)候廓潜,如果不加以判斷直接刪除鎖善榛,就會(huì)出現(xiàn)誤刪除其它請求創(chuàng)建的鎖的情況移盆,所以我們在創(chuàng)建鎖的時(shí)候需要引入一個(gè)隨機(jī)值:

//######最終判斷加鎖代碼
<?php
$random=mt_rand().uniqid(true);
$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));//對查詢進(jìn)行加鎖(一般是唯一數(shù)據(jù)項(xiàng)  例如訂單號)
if($ok){
    //相當(dāng)于業(yè)務(wù)這里一般來處理邏輯
    $cache->update();//更新緩存
    
    if ($redis->get($key) == $random) { //保證刪自己的咒循,別因?yàn)檎埱髸r(shí)間長而刪掉別人的內(nèi)容
          $redis->del($key);//處理完之后 釋放鎖
    }
}

?>

補(bǔ)充:本文在刪除鎖的時(shí)候叙甸,實(shí)際上是有問題的,沒有考慮到 GC pause 之類的問題造成的影響熔萧,比如 A 請求在 DEL 之前卡住了哪痰,然后鎖過期了,這時(shí)候 B 請求又成功獲取到了鎖,此時(shí) A 請求緩過來了肋演,就會(huì) DEL 掉 B 請求創(chuàng)建的鎖爹殊,此問題遠(yuǎn)比想象的要復(fù)雜.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末梗夸,一起剝皮案震驚了整個(gè)濱河市反症,隨后出現(xiàn)的幾起案子铅碍,更是在濱河造成了極大的恐慌线椰,老刑警劉巖憨愉,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件配紫,死亡現(xiàn)場離奇詭異笨蚁,居然都是意外死亡括细,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呆盖,“玉大人应又,你說我怎么就攤上這事株扛《淳停” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冕碟。 經(jīng)常有香客問我鸣哀,道長我衬,這世上最難降的妖魔是什么挠羔? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮范舀,結(jié)果婚禮上锭环,老公的妹妹穿的比我還像新娘辅辩。我一直安慰自己玫锋,他們只是感情好撩鹿,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布吧寺。 她就那樣靜靜地躺著,像睡著了一般获搏。 火紅的嫁衣襯著肌膚如雪常熙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機(jī)與錄音聋袋,去河邊找鬼幽勒。 笑死啥容,一個(gè)胖子當(dāng)著我的面吹牛咪惠,可吹牛的內(nèi)容都是我干的硝逢。 我是一名探鬼主播叫乌,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼排宰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盐类,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后割坠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年愉豺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了杖剪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盛嘿。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖漓库,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布赞庶,位于F島的核電站训挡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏歧强。R本人自食惡果不足惜澜薄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摊册。 院中可真熱鬧肤京,春花似錦、人聲如沸茅特。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽白修。三九已至妒峦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兵睛,已是汗流浹背肯骇。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工窥浪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笛丙。 一個(gè)月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓漾脂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親若债。 傳聞我的和親對象是個(gè)殘疾皇子唠摹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 1.1 資料 测蹲,最好的入門小冊子,可以先于一切文檔之前看,免費(fèi)饲帅。 作者Antirez的博客,Antirez維護(hù)的R...
    JefferyLcm閱讀 17,067評論 1 51
  • 飛哥薦讀本文從redis分布式鎖的官方實(shí)現(xiàn)也榄,討論了分布式鎖需要的考慮的問題惦费,并分析了RedLock、zookeep...
    Fi的學(xué)習(xí)筆記閱讀 5,626評論 0 39
  • 一泰讽、分布式鎖的作用: redis寫入時(shí)不帶鎖定功能例衍,為防止多個(gè)進(jìn)程同時(shí)進(jìn)行一個(gè)操作,出現(xiàn)意想不到的結(jié)果已卸,so......
    魔法師_閱讀 2,060評論 0 6
  • Redis的線程模型 Redis是單進(jìn)程單線程的佛玄,但是使用的是單線程非阻塞的多路IO復(fù)用的模型。多線程模型會(huì)導(dǎo)致線...
    知行_1900閱讀 616評論 0 9
  • 五種數(shù)據(jù)結(jié)構(gòu)簡介 Redis是使用C編寫的累澡,內(nèi)部實(shí)現(xiàn)了一個(gè)struct結(jié)構(gòu)體redisObject對象梦抢,通過結(jié)構(gòu)體...
    彥幀閱讀 6,947評論 0 14