概念
PHP使用分布式鎖,受語(yǔ)言本身的限制,有一些局限性。
- 通俗理解單機(jī)鎖問(wèn)題:自家的鎖鎖自家的門,只能保證自家的事,管不了別人家不鎖門引發(fā)的問(wèn)題,于是有了分布式鎖。
- 分布式鎖概念:是針對(duì)多個(gè)節(jié)點(diǎn)的鎖。避免出現(xiàn)數(shù)據(jù)不一致或者并發(fā)沖突的問(wèn)題,讓每個(gè)節(jié)點(diǎn)確保在任意時(shí)刻只有一個(gè)節(jié)點(diǎn)能夠?qū)操Y源進(jìn)行操作,單機(jī)的鎖只能夠單節(jié)點(diǎn)使用,多節(jié)點(diǎn)防不住。
- 核心原理:分布式鎖的核心原理,就是在每個(gè)節(jié)點(diǎn)執(zhí)行時(shí),先去一個(gè)公共的地方判斷是否持有鎖,如果有鎖就說(shuō)明資源被占用,沒(méi)鎖就可以持有該資源。
- 通俗舉例:多個(gè)部門,開(kāi)部門會(huì)議,需要占用會(huì)議室的位置,發(fā)現(xiàn)會(huì)議室門關(guān)著,不知道里面有沒(méi)有人,此時(shí)門外面有個(gè)牌子說(shuō)明是會(huì)議中,還是會(huì)議結(jié)束,離老遠(yuǎn)就知道會(huì)議室是不是被占用了,避免會(huì)議競(jìng)爭(zhēng)引起的錯(cuò)亂。
應(yīng)用場(chǎng)景
- 分布式排它:保證只有一個(gè)節(jié)點(diǎn)被訪問(wèn),常用于秒殺,等并發(fā)問(wèn)題的處理。
- 分布式任務(wù)調(diào)度:在分布式任務(wù)調(diào)度系統(tǒng)中,多個(gè)節(jié)點(diǎn)可能會(huì)競(jìng)爭(zhēng)執(zhí)行同一個(gè)任務(wù),使用分布式鎖可以確保只有一個(gè)節(jié)點(diǎn)能夠執(zhí)行該任務(wù),避免重復(fù)執(zhí)行和沖突。
- 并發(fā)下數(shù)據(jù)庫(kù)事務(wù)幻讀問(wèn)題:并發(fā)下的MySQL事務(wù)當(dāng)中,插入數(shù)據(jù)前先判斷有沒(méi)有,沒(méi)有再插入,從而避免重復(fù),但是其它事務(wù)未提交,就檢測(cè)不到(RR的隔離級(jí)別導(dǎo)致的),但是插入相同數(shù)據(jù),又會(huì)導(dǎo)致唯一約束起作用從而報(bào)錯(cuò),添加分布式鎖,從而避免報(bào)錯(cuò)。(這場(chǎng)景適用于唯一約束沖突報(bào)錯(cuò)很多的場(chǎng)景功能,否則使用了會(huì)影響性能)。
分布式鎖的特點(diǎn)
- 互斥性,相同時(shí)間,只能有一個(gè)節(jié)點(diǎn)會(huì)獲取該鎖,其它節(jié)點(diǎn)要么等待要么直接返回失敗。
- 可重入(單個(gè)節(jié)點(diǎn)可重復(fù)獲取該鎖且不會(huì)發(fā)生阻塞),PHP的語(yǔ)言特性不支持。
- 安全(獲取鎖的節(jié)點(diǎn)崩潰或失去連接、鎖資源會(huì)釋放)。
可用的存儲(chǔ)組件選擇
Redis、MySQL(樂(lè)觀鎖、或悲觀鎖)、ZooKeeper、Etcd、Memcache等存儲(chǔ)組件都可以實(shí)現(xiàn)分布式鎖。
ZooKeeper、Etcd是Java生態(tài),PHP幾乎不用。
Memcache很少用了,一般都會(huì)用redis。
MySQL性能比不了Redis,高并發(fā)過(guò)來(lái)容易被夯住,數(shù)據(jù)不會(huì)自動(dòng)過(guò)期刪除,需要邏輯判斷。所以也不用。
分布式鎖要求高性能,和自動(dòng)過(guò)期的兜底特性,所以用Redis的set命令剛好。
Redis分布式鎖,又稱為Redis Distributed Lock,也叫RedLock。
用Redis手動(dòng)實(shí)現(xiàn)分布式鎖(示例)
這是花十分鐘寫出來(lái)的例子,不建議商用。
class RedLock {
//聲明redis
private $redis;
/**
* @function 構(gòu)造方法初始化redis
* @other void
*/
public function __construct() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$this->redis = $redis;
}
/**
* @function 非阻塞分布式鎖
* @param $key string 鎖名稱
* @param $ttl int key自動(dòng)過(guò)期時(shí)間,單位毫秒
* @return array|false 成功返回?cái)?shù)組,失敗返回false
* @other void
*/
public function redLock($lock_name, $ttl = 10000) {
$val = base64_encode(openssl_random_pseudo_bytes(32));
$set = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);
if($set === false) {
return false;
}
return ['key' => $lock_name, 'val' => $val];
}
/**
* @function 阻塞式分布式鎖
* @param $key string 鎖名稱
* @param $ttl int key自動(dòng)過(guò)期時(shí)間,單位毫秒
* @param $ttl int 超時(shí)時(shí)間,單位毫秒
* @return array|false 成功返回?cái)?shù)組,失敗返回false
* @other void
*/
public function redLockSpin($lock_name, $ttl = 10000, $timeout = 3000) {
$start = bcmul(microtime(true), 1000, 2);
$val = base64_encode(openssl_random_pseudo_bytes(32));
$set = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);
if($set === false) {
while(true) {
//超時(shí)
$start_loop = bcmul(microtime(true), 1000, 2);
if(bcadd($start, $timeout, 2) <= $start_loop) {
return false;
}
//嘗試獲取鎖
$set_loop = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);
if($set_loop) {
return ['key' => $lock_name, 'val' => $val];
}
usleep(50000);
}
}
return ['key' => $lock_name, 'val' => $val];
}
/**
* @function 釋放鎖資源
* @param $key array|false 鎖資源
* @return bool
* @other void
*/
public function unLock($lock) {
if($lock === false) {
return false;
}
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';
if(! $this->redis->eval($script, [$lock['key'], $lock['val']], 1)) {
return false;
}
return true;
}
}
$redLock = new RedLock();
if($lock = $redLock->redLockSpin('test_key')) {
echo '搶到鎖了,處理一些業(yè)務(wù)邏輯';
} else {
echo '鎖沒(méi)有搶到';
}
$redLock->unLock($lock);
現(xiàn)有的解決方案
java實(shí)現(xiàn)分布式鎖有redisson,PHP也有自己的包。
看過(guò)一些博主的用PHP實(shí)現(xiàn)分布式鎖,好多沒(méi)有使用Lua,這沒(méi)辦法保證多條Redis語(yǔ)句原子性的執(zhí)行。
項(xiàng)目中能用到這種東西的,對(duì)于高可用、原子性、穩(wěn)定性有很強(qiáng)的依賴,所以推薦使用成熟的擴(kuò)展包。
composer require signe/redlock-php
文檔:https://packagist.org/packages/signe/redlock-php
執(zhí)行之后看使用redis的monitor指令查看,發(fā)現(xiàn)用了Lua,說(shuō)明這個(gè)包,兼顧了原子性的操作。
我這個(gè)是示例,記得無(wú)論最后執(zhí)行成功還是失敗,都記得及時(shí)釋放鎖資源。
非自旋寫法
$server = new \Redis;
$server->connect('127.0.0.1', 6379);
$servers = [$server,];
$redLock = new \RedLock\RedLock($servers);
$lock = $redLock->lock('my_resource_name', 10000);
if($lock) {
echo '加鎖成功';
這里運(yùn)行某些代碼,有個(gè)問(wèn)題,PHP缺少機(jī)制,
$redLock->unlock($lock);
} else {
echo '加鎖失敗';
}
自旋寫法
$server = new \Redis;
$server->connect('127.0.0.1', 6379);
$servers = [$server,];
$redLock = new \RedLock\RedLock($servers);
$lock = $redLock->lock('my_resource_name', 10000);
if($lock) {
echo '加鎖成功';
// $redLock->unlock($lock);
} else {
while(true) {
$lock2 = $redLock->lock('my_resource_name', 10000);
if($lock2) {
echo '加鎖成功2';
//運(yùn)行某些代碼
$redLock->unlock($lock2);
return '';
}
}
}
如果需要:拿到鎖后,釋放鎖前,業(yè)務(wù)邏輯代碼塊再對(duì)拿到鎖的分布式鎖續(xù)期。
因?yàn)閞edis的key與val值都不變,只變動(dòng)過(guò)期時(shí)間,所以使用PEXPIRE指令,也可使用PSETEX指令。
又需要防止這個(gè)鎖自動(dòng)過(guò)期,已經(jīng)被其它節(jié)點(diǎn)占用,已經(jīng)改成了其它節(jié)點(diǎn)的數(shù)據(jù),所以value值需要驗(yàn)證是不是當(dāng)前鎖的value值。
兩個(gè)操作為了保證原子性,就用到了Lua。
//$redLock = new \RedLock\RedLock($servers);
//$lock = $redLock->lock('my_resource_name', 20000);
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], KEYS[2])
else
return 0
end
';
$server->eval($script, [$lock['resource'], '毫秒過(guò)期時(shí)間', $lock['token']], 2);
PHP使用分布式鎖的局限性問(wèn)題
- 重入性無(wú)法實(shí)現(xiàn):PHP這門語(yǔ)言有局限性,不適合和redis結(jié)合做分布式鎖,分布式鎖的重入性無(wú)法實(shí)現(xiàn),因?yàn)槟_本能執(zhí)行完內(nèi)存就被回收了,無(wú)法像C/C++那樣輕松操控進(jìn)程和線程。
- 超時(shí)問(wèn)題沒(méi)有監(jiān)控機(jī)制:沒(méi)有像redisson一樣的watch dog看門狗的機(jī)制,去監(jiān)控業(yè)務(wù)執(zhí)行過(guò)長(zhǎng)導(dǎo)致redis分布式鎖自動(dòng)釋放,被其它鎖占用的問(wèn)題。
PHP使用分布式鎖,有種東施效顰的感覺(jué)。
為什么加鎖時(shí)set指令要加NX
set指令加nx表示,只有在key不存在的情況下才能設(shè)置鍵值對(duì)。
多個(gè)節(jié)點(diǎn)加鎖,獲取分布式鎖資源,實(shí)質(zhì)就是在redis中設(shè)置一條值。因?yàn)榉植际芥i的排它性,同一時(shí)間內(nèi)只能有一個(gè)節(jié)點(diǎn)可以拿到該鎖。
若用set,不加nx,就會(huì)產(chǎn)生覆蓋,造成業(yè)務(wù)錯(cuò)亂。
客戶端宕機(jī)導(dǎo)致鎖資源無(wú)法釋放的死鎖問(wèn)題
redis單線程通常不會(huì)發(fā)生死鎖問(wèn)題。
Redis在客戶端掛掉的情況的情況,會(huì)導(dǎo)致分布式鎖鎖資源無(wú)法及時(shí)釋放,這可能會(huì)導(dǎo)致其它節(jié)點(diǎn)無(wú)法加鎖從而阻塞,類似死鎖的效果。
添加過(guò)期時(shí)間做兜底即可。
對(duì)高可用:MySQL可以主從,Redis也可以,從而保證分布式鎖存儲(chǔ)的高可用性。
分布式鎖redis操作的原子性問(wèn)題
就算是redis事務(wù)(multi)也是弱事務(wù),仍舊會(huì)出現(xiàn)并發(fā)安全問(wèn)題,最好使用Lua+Redis的方式去實(shí)現(xiàn)原子性的分布式鎖,這會(huì)把一些指令集當(dāng)做一個(gè)任務(wù)隊(duì)列去處理,保證原子性。
如何設(shè)置拿到鎖資源后的超時(shí)時(shí)間
對(duì)于Java,redisson有watch dog的自動(dòng)監(jiān)控機(jī)制,但是PHP沒(méi)有。
PHP也很難實(shí)現(xiàn),原因有2:
- 不知道自動(dòng)續(xù)期的時(shí)機(jī):業(yè)務(wù)流程沒(méi)走完,分布式鎖臨近過(guò)期才續(xù)期,業(yè)務(wù)流程走完了還續(xù)什么期?這個(gè)時(shí)機(jī),高并發(fā)場(chǎng)景下難以獲取,凈增加復(fù)雜度。
- PHP語(yǔ)言本身缺少鎖機(jī)制:就算知道了要續(xù)期,加鎖與續(xù)期監(jiān)控,缺少鎖機(jī)制的強(qiáng)關(guān)聯(lián),加鎖一個(gè)進(jìn)程,監(jiān)控又一個(gè)進(jìn)程,進(jìn)程間通信是一個(gè)問(wèn)題,PHP進(jìn)程間通信與Redis操作無(wú)法原子執(zhí)行又是一個(gè)問(wèn)題,也就是說(shuō)就算被通知要續(xù)期了,再續(xù)期時(shí),鎖資源超時(shí)自動(dòng)釋放后,可能都被別的節(jié)點(diǎn)占用了。
PHP能做的只能是設(shè)置更多的超時(shí)時(shí)間,來(lái)防止鎖資源自動(dòng)釋放被其它節(jié)點(diǎn)搶走。
缺點(diǎn)也很明顯,一旦這個(gè)節(jié)點(diǎn)掛掉,鎖資源需要很長(zhǎng)時(shí)間才能釋放,這個(gè)時(shí)間段的分布式鎖無(wú)法被任意一個(gè)節(jié)點(diǎn)使用。
鎖資源的錯(cuò)誤釋放問(wèn)題
時(shí)序圖:
步驟 | 客戶端1 | 客戶端2 | 補(bǔ)充 |
---|---|---|---|
1 | 獲取鎖成功 | / | / |
2 | 執(zhí)行中 | 獲取鎖失敗 | 客戶端1的鎖阻塞了客戶端2 |
3 | 執(zhí)行中 | 獲取鎖失敗 | 客戶端2自旋,不斷嘗試獲取鎖 |
4 | 鎖資源到期自動(dòng)釋放 | 獲取鎖成功 | 由于客戶端1的鎖資源過(guò)期,才導(dǎo)致客戶端2拿到的分布式鎖 |
5 | 釋放鎖 | 執(zhí)行中 | 這一步才是客戶端1真正釋放(刪)鎖的時(shí)刻,但是由于沒(méi)做驗(yàn)證,這個(gè)釋放(刪)的過(guò)程,會(huì)把會(huì)話2創(chuàng)建的鎖給釋放(刪)掉,造成誤刪除 |
為了避免這個(gè)問(wèn)題,val值可設(shè)置為節(jié)點(diǎn)標(biāo)識(shí)。
所以redis在get值的時(shí)候,需要判斷,val值是不是當(dāng)前的節(jié)點(diǎn)標(biāo)識(shí)。
為了保證原子性,查詢和刪除兩個(gè)操作需要用Lua腳本。
其次要注意,不管節(jié)點(diǎn)程序執(zhí)行成功或者失敗,只要該走的流程走完了,都需要及時(shí)釋放鎖。
分布式鎖的可重入問(wèn)題
PHP解決不了。
假設(shè)同一個(gè)節(jié)點(diǎn),遞歸或循環(huán)添加分布式鎖,是否讓同一節(jié)點(diǎn)重復(fù)加同一把鎖,大部分場(chǎng)景不需要,但是也得看業(yè)務(wù)場(chǎng)景。
這種機(jī)制是為了避免第一層循環(huán)添加成功,之后失敗的問(wèn)題。
對(duì)于非PHP而言,重入問(wèn)題,還需要再維持一個(gè)redis hash,key為鎖名,field為節(jié)點(diǎn)的唯一標(biāo)識(shí),value為重入次數(shù),重入1次次數(shù)加1。因?yàn)橹厝胂喈?dāng)于重新獲取鎖,但是不會(huì)新增鎖資源,如果這個(gè)時(shí)間被刪掉,那么重入時(shí)會(huì)加鎖成功,但鎖資源被強(qiáng)制釋放,此時(shí)重入后的業(yè)務(wù)邏輯還不一定執(zhí)行完畢。所以刪除時(shí)需要判斷value值是否為0,如果不為0,說(shuō)明有重入,這兩步操作,也是需要再一個(gè)Lua腳本中。
分布式鎖的自旋機(jī)制
自旋可以理解為內(nèi)部死循環(huán),內(nèi)部不斷重試,直到滿足條件,直觀感受就是被阻塞。
如果沒(méi)有自旋,10個(gè)節(jié)點(diǎn),只有1個(gè)能加鎖成功,其余9個(gè)失敗,如果這9個(gè)全部失敗掉,看起來(lái)差點(diǎn)意思。
因此可以選擇被阻塞,期間不斷重試,所謂的自旋方案,其實(shí)很好理解,重試偽代碼如下:
while(加鎖失敗) {
usleep(10000);
重新嘗試加鎖代碼
if(加鎖成功) {
return '加鎖成功';
}
}
此處也可以添加一個(gè)次數(shù)限制,防止永久死循環(huán)的兜底策略
$retry_count = 0;
while(true) {
$retry_count ++;
if('加鎖成功') {
return '加鎖成功';
}
if($retry_count > 20) {
echo 1;
return '重試次數(shù)過(guò)多';
}
usleep(30000);
}
也可以根據(jù)時(shí)間去做限制,防止永久死循環(huán)的兜底策略
$start_time = microtime(true);
while('加鎖失敗') {
if(false) {
return '加鎖成功';
}
if($start_time + 5 <= microtime(true)) {
return '超時(shí)';
}
usleep(30000);
}
Redis主從架構(gòu)對(duì)分布式鎖的高可用問(wèn)題。
節(jié)點(diǎn)1再master上獲取到了分布式鎖,叫l(wèi)ock1,此時(shí)master還沒(méi)有同步到slave,結(jié)果master掛掉了。
此時(shí)故障轉(zhuǎn)移,slave做頂梁柱,節(jié)點(diǎn)2也獲取到了slave的分布式鎖,也叫l(wèi)ock1。
這種情況違背了分布式鎖的排它性。概率很小,但是有可能發(fā)生。
setnx無(wú)法解決分布式場(chǎng)景下的鎖排它性問(wèn)題。
這個(gè)是運(yùn)維層面要考慮的東西。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-842659.html
手動(dòng)實(shí)現(xiàn)分布式鎖容易被忽略的問(wèn)題
分布式鎖這種工程化的東西,每個(gè)零件都有用,雖然RedLock底層用redis set指令實(shí)現(xiàn)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-842659.html
- 若忘記加超時(shí)時(shí)間:上鎖的節(jié)點(diǎn)掛掉沒(méi)有釋放鎖資源,其它節(jié)點(diǎn)會(huì)一直拿不到鎖,嚴(yán)重影響業(yè)務(wù)。
- 若忘記加value值判斷去釋放鎖:A節(jié)點(diǎn)在執(zhí)行業(yè)務(wù)邏輯超時(shí),自動(dòng)釋放鎖資源被B節(jié)點(diǎn)搶去,等A節(jié)點(diǎn)執(zhí)行完業(yè)務(wù)代碼后釋放鎖,會(huì)把B節(jié)點(diǎn)的鎖刪除。
- 若忘記用Lua腳本:這導(dǎo)致redis在執(zhí)行任務(wù)期間,同一客戶端的多個(gè)腳本不會(huì)在一個(gè)Redis內(nèi)置的任務(wù)隊(duì)列處理,保證不了原子性,超賣的并發(fā)安全問(wèn)題就是這樣產(chǎn)生的。
- 覆蓋問(wèn)題:redis分布式鎖設(shè)置值時(shí),用的setnx思想(有值則不設(shè)置,避免覆蓋),若用set,整不好把原先的覆蓋掉了。
- 對(duì)于像Java(PHP不行)語(yǔ)言:手動(dòng)實(shí)現(xiàn)可能缺少key的監(jiān)控過(guò)期,以及重入性問(wèn)題。
到了這里,關(guān)于深入理解PHP+Redis實(shí)現(xiàn)分布式鎖的相關(guān)問(wèn)題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!