天天看點

Redis建構分布式鎖

1、前言

  為什麼要建構鎖呢?因為建構合适的鎖可以在高并發下能夠保持資料的一緻性,即用戶端在執行連貫的指令時上鎖的資料不會被别的用戶端的更改而發生錯誤。同時還能夠保證指令執行的成功率。

  看到這裡你不禁要問redis中不是有事務操作麼?事務操作不能夠實作上面的功能麼?

  的确,redis中的事務可以watch可以監控資料,進而能夠保證連貫執行的時資料的一緻性,但是我們必須清楚的認識到,在多個用戶端同時處理相同的資料的時候,很容易導緻事務的執行失敗,甚至會導緻資料的出錯。

  在關系型資料庫中,使用者首先向資料庫伺服器發送BEGIN,然後執行各個互相一緻的寫操作和讀操作,最後使用者可以選擇發送COMMIT來确認之前的修改,或者發送ROLLBACK進行復原。

  在redis中,通過特殊的指令MULTI為開始,之後使用者傳入一連貫的指令,最後EXEC為結束(在這一過程中可以使用watch進行監控一些key)。進一步分析,redis事務中的指令會先推入隊列,等到EXEC指令出現的時候才會将一條條指令執行。假若watch監控的key發生改變,這個事務将會失敗。這也就說明Redis事務中不存在鎖,其他用戶端可以修改正在執行事務中的有關資料,這也就為什麼在多個用戶端同時處理相同的資料時事務往往會發生錯誤。

回到頂部

2、簡單了解redis的單線程IO多路複用

  Redis采用單線程IO多路複用模型來實作高記憶體資料服務。何為單線程IO多路複用呢?從字面的意思可以知道redis采用的是單線程、使用的是多個IO。整個過程簡單的來講就是,哪個指令的資料流先到達就先執行。

請看下面的形象了解圖:圖中是一座窄橋,隻能允許一輛車通過,左邊是車輛進入的通道,哪一輛車先到達就先進入。即哪個IO流先到達就先處理哪個。

  Linux下網絡IO使用socket套接字來通訊,普通IO模型隻能監聽一個socket,而IO多路複用可同時監控多個socket。IO多路複用避免阻塞在IO上,單線程儲存多個socket的狀态後輪循處理。

Redis建構分布式鎖

3、并發測試

  我們就模拟一個簡單典型的并發測試,然後從這個測試中得出問題,再進一步研究。

  并發測試思路:

  1、在redis中設定一個字元串count,運用程式将其取出來加+1,再存儲回去,一直循環十萬次

  2、在兩個浏覽器上同時執行這個代碼

  3、将count取出來,檢視結果

測試步驟:

1、建立test.php檔案

Redis建構分布式鎖
 1 <?php 2 $redis=new Redis(); 3 $redis->connect('192.168.95.11','6379'); 4 for ($i=0; $i < 100000; $i++) 
 5 { 
 6   $count=$redis->get('count'); 7   $count=$count+1; 8   $redis->set('count',$count);  
 9 }10 echo "this OK";11 ?>      
Redis建構分布式鎖

2、分别在兩個浏覽器中通路test.php檔案

Redis建構分布式鎖

  結果由上圖可知,總共執行兩次,count原本應該是二十萬才對的,但實際上count等于十三萬多,遠遠小于二十萬,這是為什麼呢?

  由前面的内容可知,redis是采用單線程IO多路複用模型的。是以我們使用兩個浏覽器即為兩個會話(A、B),取出、加1、存入這三個指令并不是原子操作,并且在執行取出、存入這兩個redis指令時是哪個用戶端先到就先執行。

  例如:1、此時count=120

     2、A取出count=120,緊接着B的取出指令流到了,也将count=120取出

     3、A取出後立即加1,并将count=121存回去

     4、此時B也緊跟着,也将count=121存進去了

注意:

1、設定循環次數盡量大一點,太小的話,當在第一個浏覽器執行完畢,第二個浏覽器還沒開始進行呢

2、必須要兩個浏覽器同時執行。假若在一個浏覽器中同時執行兩次test.php檔案,不管是否同時執行,最終結果就是count=200000。因為在同一個浏覽器中執行,都是屬于同一個會話(所有指令都在同一個通道通過),是以redis會讓先執行的十萬次執行完,再接着執行其他的十萬次。

4、事務解決與原子性操作解決

  4.1、事務解決

      更改後的test.php檔案

Redis建構分布式鎖
 1 <?php 2 header("content-type: text/html;charset=utf8;"); 3 $start=time(); 4 $redis=new Redis(); 5 $redis->connect('192.168.95.11','6379'); 6  7 for ($i=0; $i < 100000; $i++) 
 8 { 
 9   $redis->multi();10   $count=$redis->get('count');11   $count=$count+1;12   $redis->set('count',$count);13   $redis->exec();14 }15 $end=time();16 echo "this OK<br/>";17 echo "執行時間為:".($end-$start);18 ?>      
Redis建構分布式鎖

執行結果失敗,表名使用事務不能夠解決此問題。

Redis建構分布式鎖

分析原因:

  我們都知道當redis開啟時,事務中的指令是不執行的,而是先将指令壓入隊列,然後當出現exec指令的時候,才會阻塞式的将所有的指令一個接一個的執行。

  是以當使用PHP中的Redis類進行redis事務的時候,所有有關redis的指令都不會真正的執行,而僅僅是将指令發送到redis中進行存儲起來。

  是以下圖中所圈到的$count實際上不是我們想要的資料,而是一個對象,是以test.php中11行出錯。

Redis建構分布式鎖

檢視對象count:

  

Redis建構分布式鎖
Redis建構分布式鎖

  4.2、原子性操作incr解決

      #更新test.php檔案

Redis建構分布式鎖
 1 <?php 2 header("content-type: text/html;charset=utf8;"); 3 $start=time(); 4 $redis=new Redis(); 5 $redis->connect('192.168.95.11','6379'); 6 for ($i=0; $i < 100000; $i++) 
 7 { 
 8   $count=$redis->incr('count'); 9 }10 $end=time();11 echo "this OK<br/>";12 echo "執行時間為:".($end-$start);13 ?>      
Redis建構分布式鎖

  兩個浏覽器同時執行,耗時14、15秒,count=200000,可以解決此問題。

缺點:

  僅僅隻是解決這裡的取出加1的問題,本質上還是沒能解決問題的,在實際環境中,我們需要做的是一系列操作,不僅僅隻是取出加1,是以就很有必要建構一個萬能鎖了。

5、建構分布式鎖  

  我們構造鎖的目的就是在高并發下消除選擇競争、保持資料一緻性

  構造鎖的時候,我們需要注意幾個問題:

    1、預防處理持有鎖在執行操作的時候程序奔潰,導緻死鎖,其他程序一直得不到此鎖

    2、持有鎖程序因為操作時間長而導緻鎖自動釋放,但本身程序并不知道,最後錯誤的釋放其他程序的鎖

    3、一個程序鎖過期後,其他多個程序同時嘗試擷取鎖,并且都成功獲得鎖

  我們将不對test.php檔案修改了,而是直接建立一個相對比較規範的面向對象Lock.class.php類檔案  

  #建立Lock.class,php檔案

Redis建構分布式鎖
  1 <?php  2 #分布式鎖  3 class Lock  4 {  5     private $redis='';  #存儲redis對象  6     /**  7     * @desc 構造函數  8     * 
  9     * @param $host string | redis主機 10     * @param $port int    | 端口 11     */ 12     public function __construct($host,$port=6379) 13     { 14         $this->redis=new Redis(); 15         $this->redis->connect($host,$port); 16     } 
 17  18     /** 19     * @desc 加鎖方法 20     * 21     * @param $lockName string | 鎖的名字 22     * @param $timeout int | 鎖的過期時間 23     * 24     * @return 成功傳回identifier/失敗傳回false 25     */ 26     public function getLock($lockName, $timeout=2) 27     { 28         $identifier=uniqid();       #擷取唯一辨別符 29         $timeout=ceil($timeout);    #確定是整數 30         $end=time()+$timeout; 31         while(time()<$end)          #循環擷取鎖 32         { 33             if($this->redis->setnx($lockName, $identifier))    #檢視$lockName是否被上鎖 34             { 35                 $this->redis->expire($lockName, $timeout);     #為$lockName設定過期時間,防止死鎖 36                 return $identifier;                             #傳回一維辨別符 37             } 38             elseif ($this->redis->ttl($lockName)===-1) 
 39             {                                40                 $this->redis->expire($lockName, $timeout);     #檢測是否有設定過期時間,沒有則加上(假設,用戶端A上一步沒能設定時間就程序奔潰了,用戶端B就可檢測出來,并設定時間) 41             } 42             usleep(0.001);         #停止0.001ms 43         } 44         return false; 45     } 46  47     /** 48     * @desc 釋放鎖 49     * 50     * @param $lockName string   | 鎖名 51     * @param $identifier string | 鎖的唯一值 52     * 53     * @param bool 54     */ 55     public function releaseLock($lockName,$identifier) 56     { 57         if($this->redis->get($lockName)==$identifier)   #判斷是鎖有沒有被其他用戶端修改 58         { 
 59             $this->redis->multi(); 60             $this->redis->del($lockName);   #釋放鎖 61             $this->redis->exec(); 62             return true; 63         } 64         else 65         { 66             return false;   #其他用戶端修改了鎖,不能删除别人的鎖 67         } 68     } 69  70     /** 71     * @desc 測試 72     * 
 73     * @param $lockName string | 鎖名 74     */ 75     public function test($lockName) 76     { 77         $start=time(); 78         for ($i=0; $i < 10000; $i++) 
 79         { 
 80             $identifier=$this->getLock($lockName); 81             if($identifier) 82             { 83               $count=$this->redis->get('count'); 84               $count=$count+1; 85               $this->redis->set('count',$count); 86               $this->releaseLock($lockName,$identifier); 87             } 
 88         } 89         $end=time(); 90         echo "this OK<br/>"; 91         echo "執行時間為:".($end-$start); 92     } 93  94 } 95  96 header("content-type: text/html;charset=utf8;"); 97 $obj=new Lock('192.168.95.11'); 98 $obj->test('lock_count'); 99 100 ?>      
Redis建構分布式鎖

 測試結果:

  在兩個不同的浏覽器中執行,最終結果count=200000,但是耗時相對較多,需要近八十多秒左右。但是在高并發下,對同一個資料,二十萬次上鎖執行釋放鎖的操作還是可以接受的,甚至已經很不錯了。

以上的簡單例子僅僅隻是為了模拟并發測試并檢驗而已,實際上我們可以使用Lock.class.php中的鎖結合自己的項目加以修改就可以很好地使用這個鎖了。例如商城中的瘋狂搶購、遊戲中虛拟商城玩家買賣東西等等。