《PHP應(yīng)用:Redis構(gòu)建分布式鎖》要點(diǎn):
本文介紹了PHP應(yīng)用:Redis構(gòu)建分布式鎖,希望對您有用。如果有疑問,可以聯(lián)系我們。
PHP學(xué)習(xí)1、前言
PHP學(xué)習(xí)為什么要構(gòu)建鎖呢?因?yàn)闃?gòu)建合適的鎖可以在高并發(fā)下能夠保持?jǐn)?shù)據(jù)的一致性,即客戶端在執(zhí)行連貫的命令時上鎖的數(shù)據(jù)不會被別的客戶端的更改而發(fā)生錯誤.同時還能夠保證命令執(zhí)行的成功率.
PHP學(xué)習(xí)看到這里你不禁要問redis中不是有事務(wù)操作么?事務(wù)操作不能夠?qū)崿F(xiàn)上面的功能么?
PHP學(xué)習(xí)的確,redis中的事務(wù)可以watch可以監(jiān)控?cái)?shù)據(jù),從而能夠保證連貫執(zhí)行的時數(shù)據(jù)的一致性,但是我們必須清楚的認(rèn)識到,在多個客戶端同時處理相同的數(shù)據(jù)的時候,很容易導(dǎo)致事務(wù)的執(zhí)行失敗,甚至?xí)?dǎo)致數(shù)據(jù)的出錯.
PHP學(xué)習(xí)在關(guān)系型數(shù)據(jù)庫中,用戶首先向數(shù)據(jù)庫服務(wù)器發(fā)送BEGIN,然后執(zhí)行各個相互一致的寫操作和讀操作,最后用戶可以選擇發(fā)送COMMIT來確認(rèn)之前的修改,或者發(fā)送ROLLBACK進(jìn)行回滾.
PHP學(xué)習(xí)在redis中,通過特殊的命令MULTI為開始,之后用戶傳入一連貫的命令,最后EXEC為結(jié)束(在這一過程中可以使用watch進(jìn)行監(jiān)控一些key).進(jìn)一步分析,redis事務(wù)中的命令會先推入隊(duì)列,等到EXEC命令出現(xiàn)的時候才會將一條條命令執(zhí)行.假若watch監(jiān)控的key發(fā)生改變,這個事務(wù)將會失敗.這也就說明Redis事務(wù)中不存在鎖,其他客戶端可以修改正在執(zhí)行事務(wù)中的有關(guān)數(shù)據(jù),這也就為什么在多個客戶端同時處理相同的數(shù)據(jù)時事務(wù)往往會發(fā)生錯誤.
PHP學(xué)習(xí)2、簡單理解redis的單線程IO多路復(fù)用
PHP學(xué)習(xí)Redis采用單線程IO多路復(fù)用模型來實(shí)現(xiàn)高內(nèi)存數(shù)據(jù)服務(wù).何為單線程IO多路復(fù)用呢?從字面的意思可以知道redis采用的是單線程、使用的是多個IO.整個過程簡單的來講就是,哪個命令的數(shù)據(jù)流先到達(dá)就先執(zhí)行.
PHP學(xué)習(xí)請看下面的形象理解圖:圖中是一座窄橋,只能允許一輛車通過,左邊是車輛進(jìn)入的通道,哪一輛車先到達(dá)就先進(jìn)入.即哪個IO流先到達(dá)就先處理哪個.
PHP學(xué)習(xí)Linux下網(wǎng)絡(luò)IO使用socket套接字來通訊,普通IO模型只能監(jiān)聽一個socket,而IO多路復(fù)用可同時監(jiān)控多個socket.IO多路復(fù)用避免阻塞在IO上,單線程保存多個socket的狀態(tài)后輪循處理.
PHP學(xué)習(xí)
PHP學(xué)習(xí)3、并發(fā)測試
PHP學(xué)習(xí)我們就模擬一個簡單典型的并發(fā)測試,然后從這個測試中得出問題,再進(jìn)一步研究.
PHP學(xué)習(xí)并發(fā)測試思路:
PHP學(xué)習(xí)1、在redis中設(shè)置一個字符串count,運(yùn)用程序?qū)⑵淙〕鰜砑?1,再存儲回去,一直循環(huán)十萬次
PHP學(xué)習(xí)2、在兩個瀏覽器上同時執(zhí)行這個代碼
PHP學(xué)習(xí)3、將count取出來,查看結(jié)果
PHP學(xué)習(xí)測試步驟:
PHP學(xué)習(xí)1、建立test.php文件
PHP學(xué)習(xí)
<?php
$redis=new Redis();
$redis->connect('192.168.95.11','6379');
for ($i=0; $i < 100000; $i++)
{
$count=$redis->get('count');
$count=$count+1;
$redis->set('count',$count);
}
echo "this OK";
?>
PHP學(xué)習(xí)2、分別在兩個瀏覽器中訪問test.php文件
PHP學(xué)習(xí)
PHP學(xué)習(xí)結(jié)果由上圖可知,總共執(zhí)行兩次,count原本應(yīng)該是二十萬才對的,但實(shí)際上count等于十三萬多,遠(yuǎn)遠(yuǎn)小于二十萬,這是為什么呢?
PHP學(xué)習(xí)由前面的內(nèi)容可知,redis是采用單線程IO多路復(fù)用模型的.因此我們使用兩個瀏覽器即為兩個會話(A、B),取出、加1、存入這三個命令并不是原子操作,并且在執(zhí)行取出、存入這兩個redis命令時是哪個客戶端先到就先執(zhí)行.
PHP學(xué)習(xí)例如:
PHP學(xué)習(xí)1、此時count=120
PHP學(xué)習(xí)2、A取出count=120,緊接著B的取出命令流到了,也將count=120取出
PHP學(xué)習(xí)3、A取出后立即加1,并將count=121存回去
PHP學(xué)習(xí)4、此時B也緊跟著,也將count=121存進(jìn)去了
PHP學(xué)習(xí)注意:
PHP學(xué)習(xí)1、設(shè)置循環(huán)次數(shù)盡量大一點(diǎn),太小的話,當(dāng)在第一個瀏覽器執(zhí)行完畢,第二個瀏覽器還沒開始進(jìn)行呢
PHP學(xué)習(xí)2、必須要兩個瀏覽器同時執(zhí)行.假若在一個瀏覽器中同時執(zhí)行兩次test.php文件,不管是否同時執(zhí)行,最終結(jié)果就是count=200000.因?yàn)樵谕粋€瀏覽器中執(zhí)行,都是屬于同一個會話(所有命令都在同一個通道通過),所以redis會讓先執(zhí)行的十萬次執(zhí)行完,再接著執(zhí)行其他的十萬次.
PHP學(xué)習(xí)4、事務(wù)解決與原子性操作解決
PHP學(xué)習(xí)4.1、事務(wù)解決
PHP學(xué)習(xí)更改后的test.php文件
PHP學(xué)習(xí)
<?php
header("content-type: text/html;charset=utf8;");
$start=time();
$redis=new Redis();
$redis->connect('192.168.95.11','6379');
for ($i=0; $i < 100000; $i++)
{
$redis->multi();
$count=$redis->get('count');
$count=$count+1;
$redis->set('count',$count);
$redis->exec();
}
$end=time();
echo "this OK<br/>";
echo "執(zhí)行時間為:".($end-$start);
?>
PHP學(xué)習(xí)執(zhí)行結(jié)果失敗,表名使用事務(wù)不能夠解決此問題.
PHP學(xué)習(xí)
PHP學(xué)習(xí)分析原因:
PHP學(xué)習(xí)我們都知道當(dāng)redis開啟時,事務(wù)中的命令是不執(zhí)行的,而是先將命令壓入隊(duì)列,然后當(dāng)出現(xiàn)exec命令的時候,才會阻塞式的將所有的命令一個接一個的執(zhí)行.
PHP學(xué)習(xí)所以當(dāng)使用PHP中的Redis類進(jìn)行redis事務(wù)的時候,所有有關(guān)redis的命令都不會真正的執(zhí)行,而僅僅是將命令發(fā)送到redis中進(jìn)行存儲起來.
PHP學(xué)習(xí)因此下圖中所圈到的$count實(shí)際上不是我們想要的數(shù)據(jù),而是一個對象,因此test.php中11行出錯.
PHP學(xué)習(xí)
PHP學(xué)習(xí)查看對象count:
PHP學(xué)習(xí)
PHP學(xué)習(xí)
PHP學(xué)習(xí)4.2、原子性操作incr解決
PHP學(xué)習(xí)#更新test.php文件
PHP學(xué)習(xí)
<?php
header("content-type: text/html;charset=utf8;");
$start=time();
$redis=new Redis();
$redis->connect('192.168.95.11','6379');
for ($i=0; $i < 100000; $i++)
{
$count=$redis->incr('count');
}
$end=time();
echo "this OK<br/>";
echo "執(zhí)行時間為:".($end-$start);
?>
PHP學(xué)習(xí)兩個瀏覽器同時執(zhí)行,耗時14、15秒,count=200000,可以解決此問題.
PHP學(xué)習(xí)缺點(diǎn):
PHP學(xué)習(xí)僅僅只是解決這里的取出加1的問題,本質(zhì)上還是沒能解決問題的,在實(shí)際環(huán)境中,我們需要做的是一系列操作,不僅僅只是取出加1,因此就很有必要構(gòu)建一個萬能鎖了.
PHP學(xué)習(xí)5、構(gòu)建分布式鎖
PHP學(xué)習(xí)我們構(gòu)造鎖的目的就是在高并發(fā)下消除選擇競爭、保持?jǐn)?shù)據(jù)一致性
PHP學(xué)習(xí)構(gòu)造鎖的時候,我們需要注意幾個問題:
PHP學(xué)習(xí)1、預(yù)防處理持有鎖在執(zhí)行操作的時候進(jìn)程奔潰,導(dǎo)致死鎖,其他進(jìn)程一直得不到此鎖
PHP學(xué)習(xí)2、持有鎖進(jìn)程因?yàn)椴僮鲿r間長而導(dǎo)致鎖自動釋放,但本身進(jìn)程并不知道,最后錯誤的釋放其他進(jìn)程的鎖
PHP學(xué)習(xí)3、一個進(jìn)程鎖過期后,其他多個進(jìn)程同時嘗試獲取鎖,并且都成功獲得鎖
PHP學(xué)習(xí)我們將不對test.php文件修改了,而是直接建立一個相對比較規(guī)范的面向?qū)ο驦ock.class.php類文件
PHP學(xué)習(xí)#建立Lock.class,php文件
PHP學(xué)習(xí)
<?php
#分布式鎖
class Lock
{
private $redis=''; #存儲redis對象
/**
* @desc 構(gòu)造函數(shù)
*
* @param $host string | redis主機(jī)
* @param $port int | 端口
*/
public function __construct($host,$port=6379)
{
$this->redis=new Redis();
$this->redis->connect($host,$port);
}
/**
* @desc 加鎖方法
*
* @param $lockName string | 鎖的名字
* @param $timeout int | 鎖的過期時間
*
* @return 成功返回identifier/失敗返回false
*/
public function getLock($lockName, $timeout=2)
{
$identifier=uniqid(); #獲取唯一標(biāo)識符
$timeout=ceil($timeout); #確保是整數(shù)
$end=time()+$timeout;
while(time()<$end) #循環(huán)獲取鎖
{
if($this->redis->setnx($lockName, $identifier)) #查看$lockName是否被上鎖
{
$this->redis->expire($lockName, $timeout); #為$lockName設(shè)置過期時間,防止死鎖
return $identifier; #返回一維標(biāo)識符
}
elseif ($this->redis->ttl($lockName)===-1)
{
$this->redis->expire($lockName, $timeout); #檢測是否有設(shè)置過期時間,沒有則加上(假設(shè),客戶端A上一步?jīng)]能設(shè)置時間就進(jìn)程奔潰了,客戶端B就可檢測出來,并設(shè)置時間)
}
usleep(0.001); #停止0.001ms
}
return false;
}
/**
* @desc 釋放鎖
*
* @param $lockName string | 鎖名
* @param $identifier string | 鎖的唯一值
*
* @param bool
*/
public function releaseLock($lockName,$identifier)
{
if($this->redis->get($lockName)==$identifier) #判斷是鎖有沒有被其他客戶端修改
{
$this->redis->multi();
$this->redis->del($lockName); #釋放鎖
$this->redis->exec();
return true;
}
else
{
return false; #其他客戶端修改了鎖,不能刪除別人的鎖
}
}
/**
* @desc 測試
*
* @param $lockName string | 鎖名
*/
public function test($lockName)
{
$start=time();
for ($i=0; $i < 10000; $i++)
{
$identifier=$this->getLock($lockName);
if($identifier)
{
$count=$this->redis->get('count');
$count=$count+1;
$this->redis->set('count',$count);
$this->releaseLock($lockName,$identifier);
}
}
$end=time();
echo "this OK<br/>";
echo "執(zhí)行時間為:".($end-$start);
}
}
header("content-type: text/html;charset=utf8;");
$obj=new Lock('192.168.95.11');
$obj->test('lock_count');
?>
PHP學(xué)習(xí)測試結(jié)果:
PHP學(xué)習(xí)在兩個不同的瀏覽器中執(zhí)行,最終結(jié)果count=200000,但是耗時相對較多,需要近八十多秒左右.但是在高并發(fā)下,對同一個數(shù)據(jù),二十萬次上鎖執(zhí)行釋放鎖的操作還是可以接受的,甚至已經(jīng)很不錯了.
PHP學(xué)習(xí)以上的簡單例子僅僅只是為了模擬并發(fā)測試并檢驗(yàn)而已,實(shí)際上我們可以使用Lock.class.php中的鎖結(jié)合自己的項(xiàng)目加以修改就可以很好地使用這個鎖了.例如商城中的瘋狂搶購、游戲中虛擬商城玩家買賣東西等等.
PHP學(xué)習(xí)以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時也希望多多支持維易PHP!
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/1312.html