您好,登錄后才能下訂單哦!
1、前言
為什么要構建鎖呢?因為構建合適的鎖可以在高并發下能夠保持數據的一致性,即客戶端在執行連貫的命令時上鎖的數據不會被別的客戶端的更改而發生錯誤。同時還能夠保證命令執行的成功率。
看到這里你不禁要問redis中不是有事務操作么?事務操作不能夠實現上面的功能么?
的確,redis中的事務可以watch可以監控數據,從而能夠保證連貫執行的時數據的一致性,但是我們必須清楚的認識到,在多個客戶端同時處理相同的數據的時候,很容易導致事務的執行失敗,甚至會導致數據的出錯。
在關系型數據庫中,用戶首先向數據庫服務器發送BEGIN,然后執行各個相互一致的寫操作和讀操作,最后用戶可以選擇發送COMMIT來確認之前的修改,或者發送ROLLBACK進行回滾。
在redis中,通過特殊的命令MULTI為開始,之后用戶傳入一連貫的命令,最后EXEC為結束(在這一過程中可以使用watch進行監控一些key)。進一步分析,redis事務中的命令會先推入隊列,等到EXEC命令出現的時候才會將一條條命令執行。假若watch監控的key發生改變,這個事務將會失敗。這也就說明Redis事務中不存在鎖,其他客戶端可以修改正在執行事務中的有關數據,這也就為什么在多個客戶端同時處理相同的數據時事務往往會發生錯誤。
回到頂部
Redis采用單線程IO多路復用模型來實現高內存數據服務。何為單線程IO多路復用呢?從字面的意思可以知道redis采用的是單線程、使用的是多個IO。整個過程簡單的來講就是,哪個命令的數據流先到達就先執行。
請看下面的形象理解圖:圖中是一座窄橋,只能允許一輛車通過,左邊是車輛進入的通道,哪一輛車先到達就先進入。即哪個IO流先到達就先處理哪個。
Linux下網絡IO使用socket套接字來通訊,普通IO模型只能監聽一個socket,而IO多路復用可同時監控多個socket。IO多路復用避免阻塞在IO上,單線程保存多個socket的狀態后輪循處理。
回到頂部
我們就模擬一個簡單典型的并發測試,然后從這個測試中得出問題,再進一步研究。
并發測試思路:
1、在redis中設置一個字符串count,運用程序將其取出來加+1,再存儲回去,一直循環十萬次
2、在兩個瀏覽器上同時執行這個代碼
3、將count取出來,查看結果
測試步驟:
1、建立test.php文件
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 ?>
2、分別在兩個瀏覽器中訪問test.php文件
結果由上圖可知,總共執行兩次,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會讓先執行的十萬次執行完,再接著執行其他的十萬次。
回到頂部
回到頂部
更改后的test.php文件
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開啟時,事務中的命令是不執行的,而是先將命令壓入隊列,然后當出現exec命令的時候,才會阻塞式的將所有的命令一個接一個的執行。
所以當使用PHP中的Redis類進行redis事務的時候,所有有關redis的命令都不會真正的執行,而僅僅是將命令發送到redis中進行存儲起來。
因此下圖中所圈到的$count實際上不是我們想要的數據,而是一個對象,因此test.php中11行出錯。
查看對象count:
回到頂部
#更新test.php文件
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 ?>
兩個瀏覽器同時執行,耗時14、15秒,count=200000,可以解決此問題。
缺點:
僅僅只是解決這里的取出加1的問題,本質上還是沒能解決問題的,在實際環境中,我們需要做的是一系列操作,不僅僅只是取出加1,因此就很有必要構建一個萬能鎖了。
回到頂部
我們構造鎖的目的就是在高并發下消除選擇競爭、保持數據一致性
構造鎖的時候,我們需要注意幾個問題:
1、預防處理持有鎖在執行操作的時候進程奔潰,導致死鎖,其他進程一直得不到此鎖
2、持有鎖進程因為操作時間長而導致鎖自動釋放,但本身進程并不知道,最后錯誤的釋放其他進程的鎖
3、一個進程鎖過期后,其他多個進程同時嘗試獲取鎖,并且都成功獲得鎖
我們將不對test.php文件修改了,而是直接建立一個相對比較規范的面向對象Lock.class.php類文件
#建立Lock.class,php文件
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 ?>
測試結果:
在兩個不同的瀏覽器中執行,最終結果count=200000,但是耗時相對較多,需要近八十多秒左右。但是在高并發下,對同一個數據,二十萬次上鎖執行釋放鎖的操作還是可以接受的,甚至已經很不錯了。
以上的簡單例子僅僅只是為了模擬并發測試并檢驗而已,實際上我們可以使用Lock.class.php中的鎖結合自己的項目加以修改就可以很好地使用這個鎖了。例如商城中的瘋狂搶購、游戲中虛擬商城玩家買賣東西等等。
(以上是自己的一些見解,若有不足或者錯誤的地方請各位指出)
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。