溫馨提示×

溫馨提示×

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

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

Redis構(gòu)建分布式鎖

發(fā)布時(shí)間:2020-07-07 02:08:55 來源:網(wǎng)絡(luò) 閱讀:226 作者:鄒君安 欄目:建站服務(wù)器

1、前言

  為什么要構(gòu)建鎖呢?因?yàn)闃?gòu)建合適的鎖可以在高并發(fā)下能夠保持?jǐn)?shù)據(jù)的一致性,即客戶端在執(zhí)行連貫的命令時(shí)上鎖的數(shù)據(jù)不會(huì)被別的客戶端的更改而發(fā)生錯(cuò)誤。同時(shí)還能夠保證命令執(zhí)行的成功率。

  看到這里你不禁要問redis中不是有事務(wù)操作么?事務(wù)操作不能夠?qū)崿F(xiàn)上面的功能么?

  的確,redis中的事務(wù)可以watch可以監(jiān)控?cái)?shù)據(jù),從而能夠保證連貫執(zhí)行的時(shí)數(shù)據(jù)的一致性,但是我們必須清楚的認(rèn)識到,在多個(gè)客戶端同時(shí)處理相同的數(shù)據(jù)的時(shí)候,很容易導(dǎo)致事務(wù)的執(zhí)行失敗,甚至?xí)?dǎo)致數(shù)據(jù)的出錯(cuò)。

  在關(guān)系型數(shù)據(jù)庫中,用戶首先向數(shù)據(jù)庫服務(wù)器發(fā)送BEGIN,然后執(zhí)行各個(gè)相互一致的寫操作和讀操作,最后用戶可以選擇發(fā)送COMMIT來確認(rèn)之前的修改,或者發(fā)送ROLLBACK進(jìn)行回滾。

  在redis中,通過特殊的命令MULTI為開始,之后用戶傳入一連貫的命令,最后EXEC為結(jié)束(在這一過程中可以使用watch進(jìn)行監(jiān)控一些key)。進(jìn)一步分析,redis事務(wù)中的命令會(huì)先推入隊(duì)列,等到EXEC命令出現(xiàn)的時(shí)候才會(huì)將一條條命令執(zhí)行。假若watch監(jiān)控的key發(fā)生改變,這個(gè)事務(wù)將會(huì)失敗。這也就說明Redis事務(wù)中不存在鎖,其他客戶端可以修改正在執(zhí)行事務(wù)中的有關(guān)數(shù)據(jù),這也就為什么在多個(gè)客戶端同時(shí)處理相同的數(shù)據(jù)時(shí)事務(wù)往往會(huì)發(fā)生錯(cuò)誤。

回到頂部

2、簡單理解redis的單線程IO多路復(fù)用

  Redis采用單線程IO多路復(fù)用模型來實(shí)現(xiàn)高內(nèi)存數(shù)據(jù)服務(wù)。何為單線程IO多路復(fù)用呢?從字面的意思可以知道redis采用的是單線程、使用的是多個(gè)IO。整個(gè)過程簡單的來講就是,哪個(gè)命令的數(shù)據(jù)流先到達(dá)就先執(zhí)行。

請看下面的形象理解圖:圖中是一座窄橋,只能允許一輛車通過,左邊是車輛進(jìn)入的通道,哪一輛車先到達(dá)就先進(jìn)入。即哪個(gè)IO流先到達(dá)就先處理哪個(gè)。

  Linux下網(wǎng)絡(luò)IO使用socket套接字來通訊,普通IO模型只能監(jiān)聽一個(gè)socket,而IO多路復(fù)用可同時(shí)監(jiān)控多個(gè)socket。IO多路復(fù)用避免阻塞在IO上,單線程保存多個(gè)socket的狀態(tài)后輪循處理。

Redis構(gòu)建分布式鎖

回到頂部

3、并發(fā)測試

  我們就模擬一個(gè)簡單典型的并發(fā)測試,然后從這個(gè)測試中得出問題,再進(jìn)一步研究。

  并發(fā)測試思路:

  1、在redis中設(shè)置一個(gè)字符串count,運(yùn)用程序?qū)⑵淙〕鰜砑?1,再存儲(chǔ)回去,一直循環(huán)十萬次

  2、在兩個(gè)瀏覽器上同時(shí)執(zhí)行這個(gè)代碼

  3、將count取出來,查看結(jié)果

測試步驟:

1、建立test.php文件

Redis構(gòu)建分布式鎖

 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構(gòu)建分布式鎖

2、分別在兩個(gè)瀏覽器中訪問test.php文件

Redis構(gòu)建分布式鎖

  結(jié)果由上圖可知,總共執(zhí)行兩次,count原本應(yīng)該是二十萬才對的,但實(shí)際上count等于十三萬多,遠(yuǎn)遠(yuǎn)小于二十萬,這是為什么呢?

  由前面的內(nèi)容可知,redis是采用單線程IO多路復(fù)用模型的。因此我們使用兩個(gè)瀏覽器即為兩個(gè)會(huì)話(A、B),取出、加1、存入這三個(gè)命令并不是原子操作,并且在執(zhí)行取出、存入這兩個(gè)redis命令時(shí)是哪個(gè)客戶端先到就先執(zhí)行。

  例如:1、此時(shí)count=120

     2、A取出count=120,緊接著B的取出命令流到了,也將count=120取出

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

     4、此時(shí)B也緊跟著,也將count=121存進(jìn)去了

注意:

1、設(shè)置循環(huán)次數(shù)盡量大一點(diǎn),太小的話,當(dāng)在第一個(gè)瀏覽器執(zhí)行完畢,第二個(gè)瀏覽器還沒開始進(jìn)行呢

2、必須要兩個(gè)瀏覽器同時(shí)執(zhí)行。假若在一個(gè)瀏覽器中同時(shí)執(zhí)行兩次test.php文件,不管是否同時(shí)執(zhí)行,最終結(jié)果就是count=200000。因?yàn)樵谕粋€(gè)瀏覽器中執(zhí)行,都是屬于同一個(gè)會(huì)話(所有命令都在同一個(gè)通道通過),所以redis會(huì)讓先執(zhí)行的十萬次執(zhí)行完,再接著執(zhí)行其他的十萬次。

 

回到頂部

4、事務(wù)解決與原子性操作解決

回到頂部

  4.1、事務(wù)解決

      更改后的test.php文件

Redis構(gòu)建分布式鎖

 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 "執(zhí)行時(shí)間為:".($end-$start);18 ?>

Redis構(gòu)建分布式鎖

執(zhí)行結(jié)果失敗,表名使用事務(wù)不能夠解決此問題。

Redis構(gòu)建分布式鎖

分析原因:

  我們都知道當(dāng)redis開啟時(shí),事務(wù)中的命令是不執(zhí)行的,而是先將命令壓入隊(duì)列,然后當(dāng)出現(xiàn)exec命令的時(shí)候,才會(huì)阻塞式的將所有的命令一個(gè)接一個(gè)的執(zhí)行。

  所以當(dāng)使用PHP中的Redis類進(jìn)行redis事務(wù)的時(shí)候,所有有關(guān)redis的命令都不會(huì)真正的執(zhí)行,而僅僅是將命令發(fā)送到redis中進(jìn)行存儲(chǔ)起來。

  因此下圖中所圈到的$count實(shí)際上不是我們想要的數(shù)據(jù),而是一個(gè)對象,因此test.php中11行出錯(cuò)。

Redis構(gòu)建分布式鎖

查看對象count:

  Redis構(gòu)建分布式鎖

  Redis構(gòu)建分布式鎖 

回到頂部

  4.2、原子性操作incr解決

      #更新test.php文件

Redis構(gòu)建分布式鎖

 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 "執(zhí)行時(shí)間為:".($end-$start);13 ?>

Redis構(gòu)建分布式鎖

  兩個(gè)瀏覽器同時(shí)執(zhí)行,耗時(shí)14、15秒,count=200000,可以解決此問題。

缺點(diǎn):

  僅僅只是解決這里的取出加1的問題,本質(zhì)上還是沒能解決問題的,在實(shí)際環(huán)境中,我們需要做的是一系列操作,不僅僅只是取出加1,因此就很有必要構(gòu)建一個(gè)萬能鎖了。

 

回到頂部

5、構(gòu)建分布式鎖  

  我們構(gòu)造鎖的目的就是在高并發(fā)下消除選擇競爭、保持?jǐn)?shù)據(jù)一致性

  構(gòu)造鎖的時(shí)候,我們需要注意幾個(gè)問題:

    1、預(yù)防處理持有鎖在執(zhí)行操作的時(shí)候進(jìn)程奔潰,導(dǎo)致死鎖,其他進(jìn)程一直得不到此鎖

    2、持有鎖進(jìn)程因?yàn)椴僮鲿r(shí)間長而導(dǎo)致鎖自動(dòng)釋放,但本身進(jìn)程并不知道,最后錯(cuò)誤的釋放其他進(jìn)程的鎖

    3、一個(gè)進(jìn)程鎖過期后,其他多個(gè)進(jìn)程同時(shí)嘗試獲取鎖,并且都成功獲得鎖

  我們將不對test.php文件修改了,而是直接建立一個(gè)相對比較規(guī)范的面向?qū)ο驦ock.class.php類文件  

  #建立Lock.class,php文件

Redis構(gòu)建分布式鎖

  1 <?php  2 #分布式鎖  3 class Lock  4 {  5     private $redis='';  #存儲(chǔ)redis對象  6     /**  7     * @desc 構(gòu)造函數(shù)  8     * 
  9     * @param $host string | redis主機(jī) 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 | 鎖的過期時(shí)間 23     * 24     * @return 成功返回identifier/失敗返回false 25     */ 26     public function getLock($lockName, $timeout=2) 27     { 28         $identifier=uniqid();       #獲取唯一標(biāo)識符 29         $timeout=ceil($timeout);    #確保是整數(shù) 30         $end=time()+$timeout; 31         while(time()<$end)          #循環(huán)獲取鎖 32         { 33             if($this->redis->setnx($lockName, $identifier))    #查看$lockName是否被上鎖 34             { 35                 $this->redis->expire($lockName, $timeout);     #為$lockName設(shè)置過期時(shí)間,防止死鎖 36                 return $identifier;                             #返回一維標(biāo)識符 37             } 38             elseif ($this->redis->ttl($lockName)===-1) 
 39             {                                40                 $this->redis->expire($lockName, $timeout);     #檢測是否有設(shè)置過期時(shí)間,沒有則加上(假設(shè),客戶端A上一步?jīng)]能設(shè)置時(shí)間就進(jìn)程奔潰了,客戶端B就可檢測出來,并設(shè)置時(shí)間) 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 "執(zhí)行時(shí)間為:".($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構(gòu)建分布式鎖

 測試結(jié)果:

  在兩個(gè)不同的瀏覽器中執(zhí)行,最終結(jié)果count=200000,但是耗時(shí)相對較多,需要近八十多秒左右。但是在高并發(fā)下,對同一個(gè)數(shù)據(jù),二十萬次上鎖執(zhí)行釋放鎖的操作還是可以接受的,甚至已經(jīng)很不錯(cuò)了。

以上的簡單例子僅僅只是為了模擬并發(fā)測試并檢驗(yàn)而已,實(shí)際上我們可以使用Lock.class.php中的鎖結(jié)合自己的項(xiàng)目加以修改就可以很好地使用這個(gè)鎖了。例如商城中的瘋狂搶購、游戲中虛擬商城玩家買賣東西等等。

 

(以上是自己的一些見解,若有不足或者錯(cuò)誤的地方請各位指出)


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

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

AI