天天看點

使用redis的比較完美的加鎖解鎖使用redis的比較完美的加鎖解鎖

使用redis的比較完美的加鎖解鎖

tags:redis read&write redis加鎖和解鎖 php

習慣性說一下寫這篇文章要說明什麼,我們經常用redis進行加鎖操作,目的是為了解決并發可能帶來的問題。但是使用redis加鎖的方式有多種,本文對常見的幾種方式進行解析,并提供一種相對完美的方案。

read & write 問題

這是一個經典問題,請看代碼:

//redis中的某個鍵自增
    $val = $this->redis->get($key);
    $val ++;
    $this->redis->set($val);           

這段代碼邏輯沒有問題,就是先讀取資料,再修改資料,在寫回修改,這裡是希望每次通路都遞增變量$val的值,但在并發情況下,存在情況是兩個程序都讀取到了一樣的初始值,然後都加1,最後寫回Redis,這種情況就會統計資料比實際的少。這個問題應該有許多人遇到過,思考過怎麼解決這類問題。這裡給出一個統一的解決方案,就是盡量保證操作的原子性,比如可以用redis的incr指令來實作自增(可以認為redis的指令是原子的)。

加鎖

由上面的問題再進一步,來探讨一個大家常用的,為一個操作進行加鎖。

問題場景如下:有一個商品,每個使用者都可以去修改商品資訊。假設使用者id分别為6和8的使用者對id為123的商品進行操作。

錯誤示例1

$key = '123';
    $val = $this->redis->get($key);
    if(!$val){
        $this->redis->set($key,'123');
        $this->redis->expire($key,'4');
        /**此處修改商品資訊操作
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '錯誤提示';
    }
               

上面這個錯誤示例,

錯誤點1:set和expire是分開寫的,如果說程式執行中再執行了set()後出現崩潰,則這個就變成了永久鎖(雖然這是個小機率事件)。

錯誤點2:這個商品中設定的key是商品id,val也是商品id,很多人認為隻有一個key就可以了,val是什麼無所謂。這就缺少了鎖的辨別,無法判斷這個鎖的擁有者是誰,進而會帶來一系列影響如下。

  1. 使用者1程序擷取key對應的val,發現沒有鎖,是以調用了set,可能在set前,另一個使用者2的程序也發現沒有這個鎖,也進行set,就造成了兩個程序都認為自己擷取到了鎖的情況,
  2. 然後繼續,如果1使用者的程序執行完了操作,删除了key,使用者2程序未執行完畢,此時由于無法識别是否是自己加的鎖,就删除了key,這時再有新的程序進入,檢查不到鎖,可以立即執行,則有可能和使用者2的修改沖突。

針對錯誤1和錯誤2的第1點,我們隻需要去除read & write模式就可以解決,解決方案為

//同時設定val和過期時間,并使用setnx
    $status = $this->redis->setnx($key,$val,$expireTime);
    if($status){
         /**此處修改商品資訊操作
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '錯誤提示';
    }           
setnx,可以在設定時檢查是否存在鎖不存在則設定并傳回1,如果存在不覆寫并傳回0。

針對錯誤2第2點,我們需要為每個程序設定一個獨立的自己可以識别的val,如果一個使用者隻能開一個程序,這個val可以為使用者id,如果一個使用者可以設定多個程序,那麼必須按照實際車情況采用其他方式來區分,這裡我們以使用者id為例,并且在删除的時候隻能删除自己的鎖。那麼這裡問題又出現了,如果我們寫成這樣:

//同時設定val和過期時間,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此處修改商品資訊操作
                ******
        **/
        if($this->redis->get($key) == $userId){
            $this->redis->del($key);
        }
        
    }else{
        echo '錯誤提示';
    }           
這種情況看似沒有什麼問題,其實不然,大家注意我再設定所得時候,設定了一個過期時間,假如這個時間設定的是4秒,那麼如果程序A執行到删除前一刻一不小心超過了4秒,那麼這個鎖就自動消失了。而另一個程序B查到沒有鎖,就加了一把自己的鎖,此時程序A執行删除,就把B的鎖給删除了(極小機率事件)。

這裡解決方案有兩種

  1. 設定比較長的expire時間,弊端:設定的太長,占用記憶體時間長,設定的太短不能完全解決問題。(可能有人會想不設定過期時間就可以,那麼回到最初的錯誤點,如果程式設定了鎖後崩潰了就變成了永久的鎖。)
  2. 把對比和删除弄成一個原子操作,這裡呢找到了一個方法,就是用redis的eval,把語句變成原子操作。注意redis用的是lua文法,我也是新學的
//同時設定val和過期時間,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此處修改商品資訊操作
                ******
        **/
        //因為寫這個部落格的機器沒有裝redis,是以沒有驗證這個文法對不對。請大家見諒
         $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        $result = $this->redis->eval(script,array($key,$val),1);
        if ($result) {
            return true;
        }

    }else{
        echo '錯誤提示';
    }           

這裡就把兩個操作變成了一個原子操作。解決的加鎖和解鎖可能出現的問題。

我們來說一些題外話拓展:在程序有可能出現沖突的地方,一般我們叫做臨界區(作業系統中也有這個概念,是通過另一種叫做PV信号量的方式來解決的,其實可以了解為組織等待程序隊列,P操作不能擷取到資源使用權的則進入等待隊列,等待V操作釋放資源後,檢查是否有等待隊列,進行程序釋放。當然PV操作也是原子性的。是以說解決相似問題的辦法也有一定的相似性)。
歡迎大家評論補充   ---  vinter_he           

希望大家多評論交流,互相學習

繼續閱讀