Redis實現分布式鎖 php


一、分布式鎖的作用:

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

 

二、Redis的NX后綴命令

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

 

三、實現原理

    在進程請求執行操作前進行判斷,加鎖是否成功,加鎖成功允許執行下步操作;

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

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

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

 

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

$expire = 10;//有效期10秒
$key = 'lock';//key
$value = time() + $expire;//鎖的值 = Unix時間戳 + 鎖的有效期
$lock = $redis->setnx($key, $value);
//判斷是否上鎖成功,成功則執行下步操作
if(!empty($lock))
{
     //下步操作...       
}

如果返回 1 ,則表示當前進程獲得鎖,並獲得了當前插入/更新緩存的操作權限。

如果返回 0,表示鎖已被其他進程獲取,這是進程可以返回結果或者等待當前鎖失效再請求。

五、解決死鎖

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

解決方法是在獲取鎖失敗的同時獲取鎖的值,並將值與當前時間進行對比,如果值小於當前時間說明鎖以過期失效,進程可運用Redis的DEL命令刪除該鎖。

$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命令上鎖也會出現問題。比如,進程1獲得鎖后崩潰或刪除鎖失敗,這時進程2檢測到鎖存在當已過期,用DEL命令刪除鎖並用SETNX命令設置鎖,進程3也檢測到鎖過期,也用DEL命令刪除鎖也用SETNX命令設置了鎖,這時進程2和進程3同時獲得了鎖。問題大了!

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

  例如:

    進程1獲得鎖后操作超時/崩潰/刪除鎖失敗,

    進程2檢測到鎖已存在,但獲取鎖的值對比當前時間發現鎖已過期,

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

    進程2完成下步操作后返回前應該刪除鎖,但在刪除鎖時可以先檢測鎖是否還未過期,未過期才做刪除操作,已過期的就沒必要在去刪除鎖了,因為很有可能其他進程檢測到鎖過期時已經去獲取鎖了。

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

以下是Redis實現分布式鎖的完整PHP代碼:

<?php
/**
 * 實現Redis分布鎖
 */
 
$key        = 'test';       //要更新信息的緩存KEY
$lockKey    = 'lock:'.$key; //設置鎖KEY
$lockExpire = 10;           //設置鎖的有效期為10秒
 
//獲取緩存信息
$result = $redis->get($key);
//判斷緩存中是否有數據
if(empty($result))
{
    $status = TRUE;
    while ($status)
    {
        //設置鎖值為當前時間戳 + 有效期
        $lockValue = time() + $lockExpire;
        /**
         * 創建鎖
         * 試圖以$lockKey為key創建一個緩存,value值為當前時間戳
         * 由於setnx()函數只有在不存在當前key的緩存時才會創建成功
         * 所以,用此函數就可以判斷當前執行的操作是否已經有其他進程在執行了
         * @var [type]
         */
        $lock = $redis->setnx($lockKey, $lockValue);
        /**
         * 滿足兩個條件中的一個即可進行操作
         * 1、上面一步創建鎖成功;
         * 2、   1)判斷鎖的值(時間戳)是否小於當前時間    $redis->get()
         *      2)同時給鎖設置新值成功    $redis->getset()
         */
        if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))
        {
            //給鎖設置生存時間
            $redis->expire($lockKey, $lockExpire);
            //******************************
            //此處執行插入、更新緩存操作...
            //******************************
 
            //以上程序走完刪除鎖
            //檢測鎖是否過期,過期鎖沒必要刪除
            if($redis->ttl($lockKey))
                $redis->del($lockKey);
            $status = FALSE;
        }else{
            /**
             * 如果存在有效鎖這里做相應處理
             *      等待當前操作完成再執行此次請求
             *      直接返回
             */
            sleep(2);//等待2秒后再嘗試執行操作
        }
    }
} 

 

實現分布式鎖用到的Redis命令介紹:

setnx(key, value)

    將key的值設為value,當且僅當key不存在。

    若給定的key已經存在,則SETNX不做任何動作。

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

    返回值:

        設置成功,返回1。

        設置失敗,返回0。

 

    get(key)

    返回key所關聯的字符串值。

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

    假如key儲存的值不是字符串類型,返回一個錯誤,因為GET只能用於處理字符串值。

    返回值:

        key的值。

        如果key不存在,返回nil。

 

    getset(key, value)

        將給定key的值設為value,並返回key的舊值。

        當key存在但不是字符串類型時,返回一個錯誤。

        返回值:

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

            當key沒有舊值時,返回nil。

 

    expire(key, seconds)

    為給定key設置生存時間。

    當key過期時,它會被自動刪除。

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

    在低於2.1.3版本的Redis中,已存在的生存時間不可覆蓋。

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

    返回值:

        設置成功返回1。

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

 

    ttl(key)

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

    返回值:

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

        當key不存在或沒有設置生存時間時,返回-1 。

 

    del(key)

    移除給定的一個或多個key。

    返回值:

        被移除key的數量。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM