Redis實(shí)現(xiàn)分布式鎖 php

一、分布式鎖的作用:

redis寫入時不帶鎖定功能,為防止多個進(jìn)程同時進(jìn)行一個操作,出現(xiàn)意想不到的結(jié)果,so...對緩存進(jìn)行插入更新操作時自定義加鎖功能。

二、Redis的NX后綴命令

Redis有一系列的命令,其特點(diǎn)是以NX結(jié)尾,NX的意思可以理解為 NOT EXISTS(不存在),SETNX命令 (SET IF NOT EXISTS) 可以理解為如果不存在則插入,Redis分布式鎖的實(shí)現(xiàn)主要就是使用SETNX命令。

三、實(shí)現(xiàn)原理

在進(jìn)程請求執(zhí)行操作前進(jìn)行判斷,加鎖是否成功,加鎖成功允許執(zhí)行下步操作;

如果不成功,則判斷鎖的值(時間戳)是否大于當(dāng)前時間,如果大于當(dāng)前時間,則獲取鎖失敗不允許執(zhí)行下步操作;

如果鎖的值(時間戳)小于當(dāng)前時間,并且GETSET命令獲取到的鎖的舊值依然小于當(dāng)前時間,則獲取鎖成功允許執(zhí)行下步操作;

如果鎖的值(時間戳)小于當(dāng)前時間,并且GETSET命令獲取到的鎖的舊值大于當(dāng)前時間,則獲取鎖失敗不允許執(zhí)行下步操作;

四、$redis->setnx() 設(shè)置鎖

<?php

$expire = 10;//有效期10秒

$key = 'lock';//key

$value = time() + $expire;//鎖的值 = Unix時間戳 + 鎖的有效期

$lock = $redis->setnx($key, $value);

//判斷是否上鎖成功,成功則執(zhí)行下步操作

if(!empty($lock))

{

//下步操作...

}

?>

如果返回 1 ,則表示當(dāng)前進(jìn)程獲得鎖,并獲得了當(dāng)前插入/更新緩存的操作權(quán)限。

如果返回 0,表示鎖已被其他進(jìn)程獲取,這是進(jìn)程可以返回結(jié)果或者等待當(dāng)前鎖失效再請求。

五、解決死鎖

如果只用SETNX命令設(shè)置鎖的話,如果當(dāng)持有鎖的進(jìn)程崩潰或刪除鎖失敗時,其他進(jìn)程將無法獲取到鎖,問題就大了。

解決方法是在獲取鎖失敗的同時獲取鎖的值,并將值與當(dāng)前時間進(jìn)行對比,如果值小于當(dāng)前時間說明鎖以過期失效,進(jìn)程可運(yùn)用Redis的DEL命令刪除該鎖。

<?php

$expire = 10;//有效期10秒

$key = 'lock';//key

$value = time() + $expire;//鎖的值 = Unix時間戳 + 鎖的有效期

$status = true;

while($status)

{

? ? $lock = $redis->setnx($key, $value);

? ? if(empty($lock))

? ? {

? ? $value = $redis->get($key);

? ? ? ? if($value < time())

? ? ? ? {

? ? ? ? ? ? ? ? $redis->del($key);

? ? ? ? }

? ? }else{

? ? ? ? $status = false;

? ? ? ? //下步操作....

? ? }

}

?>

但是,簡單粗暴的用DEL命令刪除鎖再SETNX命令上鎖也會出現(xiàn)問題。比如,進(jìn)程1獲得鎖后崩潰或刪除鎖失敗,這時進(jìn)程2檢測到鎖存在當(dāng)已過期,用DEL命令刪除鎖并用SETNX命令設(shè)置鎖,進(jìn)程3也檢測到鎖過期,也用DEL命令刪除鎖也用SETNX命令設(shè)置了鎖,這時進(jìn)程2和進(jìn)程3同時獲得了鎖。問題大了!

為了解決這個問題,這里用到了Redis的GETSET命令,GETSET命令在給鎖設(shè)置新值的同時返回鎖的舊值,這里利用了GETSET命令同時獲取和賦值的特性,在此期間其他進(jìn)程無法修改鎖的值。

例如:

進(jìn)程1獲得鎖后操作超時/崩潰/刪除鎖失敗,

進(jìn)程2檢測到鎖已存在,但獲取鎖的值對比當(dāng)前時間發(fā)現(xiàn)鎖已過期,

進(jìn)程2通過GETSET命令重新給鎖賦予新的值,并獲取到的鎖的舊值,再次對比鎖的舊值與當(dāng)前時間,如果鎖的舊值依然小于當(dāng)前時間的話,這時進(jìn)程2就可以忽略進(jìn)程1余留下的廢鎖進(jìn)行下步操作了。

進(jìn)程2完成下步操作后返回前應(yīng)該刪除鎖,但在刪除鎖時可以先檢測鎖是否還未過期,未過期才做刪除操作,已過期的就沒必要在去刪除鎖了,因?yàn)楹苡锌赡芷渌M(jìn)程檢測到鎖過期時已經(jīng)去獲取鎖了。

這里要說明的是,如果有其他進(jìn)程在進(jìn)程2之前獲取到鎖,那么進(jìn)程2將獲取鎖失敗,但是進(jìn)程2在用GETSET獲取鎖的舊值時也賦予了鎖新的值,改寫了其他進(jìn)程賦予鎖的超時值??吹竭@大家可能會有疑問了,進(jìn)程2沒獲取到鎖怎么能改變鎖的值呢?是的,進(jìn)程2改變了鎖的原有值,但這一點(diǎn)小小的時間誤差帶來的影響是可以忽略。

以下是Redis實(shí)現(xiàn)分布式鎖的完整PHP代碼:

<?php

get($key);

//判斷緩存中是否有數(shù)據(jù)

if(empty($result))

{

$status = TRUE;

while ($status)

{

//設(shè)置鎖值為當(dāng)前時間戳 + 有效期

$lockValue = time() + $lockExpire;

/**

* 創(chuàng)建鎖

* 試圖以$lockKey為key創(chuàng)建一個緩存,value值為當(dāng)前時間戳

* 由于setnx()函數(shù)只有在不存在當(dāng)前key的緩存時才會創(chuàng)建成功

* 所以,用此函數(shù)就可以判斷當(dāng)前執(zhí)行的操作是否已經(jīng)有其他進(jìn)程在執(zhí)行了

* @var [type]

*/

$lock = $redis->setnx($lockKey, $lockValue);

/**

* 滿足兩個條件中的一個即可進(jìn)行操作

* 1、上面一步創(chuàng)建鎖成功;

* 2、? 1)判斷鎖的值(時間戳)是否小于當(dāng)前時間? ? $redis->get()

* ? ? ? ? 2)同時給鎖設(shè)置新值成功? ? $redis->getset()

*/

if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))

{

? ? ?//給鎖設(shè)置生存時間

? ? $redis->expire($lockKey, $lockExpire);

? ? //******************************

? ? //此處執(zhí)行插入、更新緩存操作...

? ? //******************************

? ? //以上程序走完刪除鎖

? ? //檢測鎖是否過期,過期鎖沒必要刪除

? ? if($redis->ttl($lockKey))

? ? ? ? ?$redis->del($lockKey);

? ? ? ? $status = FALSE;

? ? ? ? }else{

? ? ? ? ? ? ? /**

? ? ? ? ? ? ? ? ?* 如果存在有效鎖這里做相應(yīng)處理

? ? ? ? ? ? ? ? ?*? ? ? 等待當(dāng)前操作完成再執(zhí)行此次請求

? ? ? ? ? ? ? ? ? *? ? ? 直接返回

? ? ? ? ? ? ? ? ? */

? ? ? ? ? ? ? ? ? sleep(2);//等待2秒后再嘗試執(zhí)行操作

? ? ? ? }

? ? }

}

?>

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

相關(guān)閱讀更多精彩內(nèi)容

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