溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

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

發(fā)布時(shí)間:2021-07-05 16:26:56 來源:億速云 閱讀:130 作者:chen 欄目:編程語言

本篇內(nèi)容主要講解“php與Redis如何實(shí)現(xiàn)分布式鎖”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“php與Redis如何實(shí)現(xiàn)分布式鎖”吧!

一、分布式鎖的作用:

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

二、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)程請(qǐng)求執(zhí)行操作前進(jìn)行判斷,加鎖是否成功,加鎖成功允許執(zhí)行下步操作;

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

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

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

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

$expire = 10;//有效期10秒
$key = 'lock';//key
$value = time() + $expire;//鎖的值 = Unix時(shí)間戳 + 鎖的有效期
$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)前鎖失效再請(qǐng)求。

五、解決死鎖

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

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

$expire = 10;//有效期10秒
$key = 'lock';//key
$value = time() + $expire;//鎖的值 = Unix時(shí)間戳 + 鎖的有效期
$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;
        //下步操作....
    }
}

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

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

  例如:

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

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

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

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

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

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

<?php
/**
 * 實(shí)現(xiàn)Redis分布鎖
 */

$key        = 'test';       //要更新信息的緩存KEY
$lockKey    = 'lock:'.$key; //設(shè)置鎖KEY
$lockExpire = 10;           //設(shè)置鎖的有效期為10秒

//獲取緩存信息
$result = $redis->get($key);
//判斷緩存中是否有數(shù)據(jù)
if(empty($result))
{
    $status = TRUE;
    while ($status)
    {
        //設(shè)置鎖值為當(dāng)前時(shí)間戳 + 有效期
        $lockValue = time() + $lockExpire;
        /**
         * 創(chuàng)建鎖
         * 試圖以$lockKey為key創(chuàng)建一個(gè)緩存,value值為當(dāng)前時(shí)間戳
         * 由于setnx()函數(shù)只有在不存在當(dāng)前key的緩存時(shí)才會(huì)創(chuàng)建成功
         * 所以,用此函數(shù)就可以判斷當(dāng)前執(zhí)行的操作是否已經(jīng)有其他進(jìn)程在執(zhí)行了
         * @var [type]
         */
        $lock = $redis->setnx($lockKey, $lockValue);
        /**
         * 滿足兩個(gè)條件中的一個(gè)即可進(jìn)行操作
         * 1、上面一步創(chuàng)建鎖成功;
         * 2、   1)判斷鎖的值(時(shí)間戳)是否小于當(dāng)前時(shí)間    $redis->get()
         *      2)同時(shí)給鎖設(shè)置新值成功    $redis->getset()
         */
        if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))
        {
            //給鎖設(shè)置生存時(shí)間
            $redis->expire($lockKey, $lockExpire);
            //******************************
            //此處執(zhí)行插入、更新緩存操作...
            //******************************

            //以上程序走完刪除鎖
            //檢測(cè)鎖是否過期,過期鎖沒必要?jiǎng)h除
            if($redis->ttl($lockKey))
                $redis->del($lockKey);
            $status = FALSE;
        }else{
            /**
             * 如果存在有效鎖這里做相應(yīng)處理
             *      等待當(dāng)前操作完成再執(zhí)行此次請(qǐng)求
             *      直接返回
             */
            sleep(2);//等待2秒后再嘗試執(zhí)行操作
        }
    }
}

實(shí)現(xiàn)分布式鎖用到的Redis命令介紹:

setnx(key, value)

將key的值設(shè)為value,當(dāng)且僅當(dāng)key不存在。

若給定的key已經(jīng)存在,則SETNX不做任何動(dòng)作。

SETNX是”SET if Not eXists”(如果不存在,則SET)的簡(jiǎn)寫。

返回值:

設(shè)置成功,返回1。

設(shè)置失敗,返回0。

get(key)

返回key所關(guān)聯(lián)的字符串值。

如果key不存在則返回特殊值nil。

假如key儲(chǔ)存的值不是字符串類型,返回一個(gè)錯(cuò)誤,因?yàn)镚ET只能用于處理字符串值。

返回值:

key的值。

如果key不存在,返回nil。

getset(key, value)

將給定key的值設(shè)為value,并返回key的舊值。

當(dāng)key存在但不是字符串類型時(shí),返回一個(gè)錯(cuò)誤。

返回值:

返回給定key的舊值(old value)。

當(dāng)key沒有舊值時(shí),返回nil。

expire(key, seconds)

為給定key設(shè)置生存時(shí)間。

當(dāng)key過期時(shí),它會(huì)被自動(dòng)刪除。

在Redis中,帶有生存時(shí)間的key被稱作“易失的”(volatile)。

在低于2.1.3版本的Redis中,已存在的生存時(shí)間不可覆蓋。

從2.1.3版本開始,key的生存時(shí)間可以被更新,也可以被PERSIST命令移除。(詳情參見 http://redis.io/topics/expire)。

返回值:

設(shè)置成功返回1。

當(dāng)key不存在或者不能為key設(shè)置生存時(shí)間時(shí)(比如在低于2.1.3中你嘗試更新key的生存時(shí)間),返回0。

ttl(key)

返回給定key的剩余生存時(shí)間(time to live)(以秒為單位)。

返回值:

key的剩余生存時(shí)間(以秒為單位)。

當(dāng)key不存在或沒有設(shè)置生存時(shí)間時(shí),返回-1 。

del(key)

移除給定的一個(gè)或多個(gè)key。

返回值:

被移除key的數(shù)量。

到此,相信大家對(duì)“php與Redis如何實(shí)現(xiàn)分布式鎖”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI