基于PHP+Redis實現(xiàn)分布式鎖
一、Redis作為分布式鎖的優(yōu)勢
Redis是一個開源的、基于內(nèi)存的鍵值存儲系統(tǒng),它支持多種數(shù)據(jù)結構并具備持久化選項。由于其提供了原子操作(如SETNX、EXPIRE等)和高性能特性,使得Redis成為實現(xiàn)分布式鎖的理想選擇:
- 性能優(yōu)異:Redis是內(nèi)存數(shù)據(jù)庫,響應速度極快,適合于高頻讀寫的場景。
- 原子性:Redis對某些命令(如
SETNX)提供了原子操作,還可以執(zhí)行l(wèi)ua腳本,所以確保了業(yè)務的穩(wěn)定性。 - 超時釋放:可以設置鎖的有效期,即使持有鎖的進程崩潰,也能通過過期機制自動釋放鎖,避免死鎖問題。
二、PHP中使用Redis實現(xiàn)分布式鎖的步驟與原理
前期準備
- 運行環(huán)境:
php 7.3.4+phpredis擴展 4.3.0+redis windows客戶端 3.2.100- phpredis擴展文檔
- 簡單了解lua腳本
在使用分布式鎖時候我們首先要考慮以下幾點:
- 如何確保鎖的唯一性?
使用phpredis擴展的 setNx('key','value') 或者使用 set('key', 'value', ['nx', 'ex'=>10]) # Will set the key, if it doesn't exist, with a ttl of 10 second 方法,這些方法保證這個key不存在于redis數(shù)據(jù)庫時才會寫入,就算有N個并發(fā)同時在寫這個key,redis也能確保只會有一個能寫成功。 - 如何避免死鎖?
死鎖一般發(fā)生在我們的業(yè)務代碼拋出異?;蛘邎?zhí)行超時,最終沒有釋放鎖從而導致產(chǎn)生了死鎖。這種情況我們可以通過增加一個鎖的有效期就能避免產(chǎn)生死鎖。例如:- 使用redis的expire方法給對應的key設置一個有效期 expire(string $key, int $seconds, ?string $mode = NULL): Redis|bool
- 使用lua腳本 redis.call("expire", KEYS[1], ARGV[2])
- 如何確保redis命令執(zhí)行的原子性?
要保證原子性必須要求一系列操作要么全部成功執(zhí)行,要么全部不執(zhí)行。舉例:
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
$result = $redis->setNx('key','val');
if ($result) {
$redis->expire('key',30);
}
上面的代碼看起來沒有太大的問題,但是 $redis->expire() 一旦執(zhí)行失敗就創(chuàng)建了一個不過期的值,最終就可能導致產(chǎn)生死鎖,這就是為什么要保證命令執(zhí)行的原子性。
我們可以通過 $redis->eval() 方法執(zhí)行 lua腳本 來解決這個問題(我們不用關心實現(xiàn)細節(jié),這是底層的實現(xiàn),只需要知道要保證 redis 命令執(zhí)行的原子性用lua腳本就行)。示例:
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
$luaScript = <<<LUA
if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
redis.call("expire", KEYS[1], ARGV[2])
return true
end
return false
LUA;
$result = $redis>eval($luaScript,[ $this->lockKey, $this->requestId, $this->expireTime ],1);
eval 方法使用詳解,官方的文檔和示例寫得有點打腦殼,完全沒寫腳本字符串中的 KEYS 和 ARGV 和傳遞參數(shù)的對應關系。下面寫了一個對應關系的例子方便大家理解:
語法:$redis>eval(string $script, ?array $args, ?int num_keys): mixed
參數(shù)說明:
- string $script 執(zhí)行的lua腳本字符串
- ?array $args lua腳本字符串中
KEYS和ARGV的對應值,按順序對應(可選值) - ?int num_keys lua腳本字符串中
KEYS的數(shù)量,寫了幾個KEYS就傳幾個(可選值)
官方文檔eval方法說明:

//index.php
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
$luaScript = <<<LUA
return {KEYS[1],KEYS[2],KEYS[3],ARGV[1],ARGV[2]};
LUA;
var_dump($redis->eval($luaScript,[1,2,3,4,5],3));
輸出結果

以下是完整的實現(xiàn)代碼:
- RedisDistributedLock.php
<?php
class RedisDistributedLock {
private $redis;
private $lockKey;
private $requestId;
private $expireTime;
/**
* @param string $lockKey 加鎖的key
* @param int $expireTime 鎖的有效期(單位:秒)
*/
public function __construct(string $lockKey, $expireTime = 30)
{
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
$this->redis = $redis;
$this->lockKey = $lockKey;
$this->expireTime = $expireTime;
$this->requestId = uniqid(); // 生成唯一請求ID
}
/**
* 嘗試獲取鎖,并在指定次數(shù)內(nèi)進行重試
*
* @param int $maxRetries 最大重試次數(shù),默認為3次
* @param int $retryDelay 兩次重試之間的延遲時間(單位:毫秒)
* @return bool 是否成功獲取鎖
*/
public function acquireLock(int $maxRetries = 3, int $retryDelay = 50): bool
{
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
if ($this->acquireLockOnce()) {
return true;
}
usleep($retryDelay * 1000);
}
return false;
}
/**
* 進行加鎖
* @return bool 加鎖是否成功
*/
private function acquireLockOnce(): bool
{
$luaScript = <<<LUA
if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
redis.call("expire", KEYS[1], ARGV[2])
return true
end
return false
LUA;
$result = $this->redis->eval(
$luaScript,
[ $this->lockKey, $this->requestId, $this->expireTime ],
1
);
return (bool)$result;
}
/**
* 釋放鎖
* @return bool
*/
public function releaseLock(): bool
{
$luaScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
$result = $this->redis->eval(
$luaScript,
[ $this->lockKey, $this->requestId ],
1
);
return (bool)$result;
}
}
?>
- index.php
<?php
include 'RedisDistributedLock.php';
function task() {
$lockKey = 'task_1';
$handler = new RedisDistributedLock($lockKey);
$startTime = time();
if ($handler->acquireLock(4)) {
//@TODO 加鎖成功后執(zhí)行具體的業(yè)務邏輯
echo '加鎖成功 開始執(zhí)行加鎖邏輯的時間:'.date('Y-m-d H:i:s',$startTime);
echo "\r\n";
echo '鎖定到:'.date('Y-m-d H:i:s',time() + 15);
sleep(15);
$handler->releaseLock();
echo "\r\n";
echo '---15s后已釋放鎖---';
} else {
echo '加鎖失?。?.date('Y-m-d H:i:s',$startTime);
return false;
}
}
task();
?>
執(zhí)行結果如下:

三、待優(yōu)化的地方
- 集群環(huán)境下如果主節(jié)點掛掉,如何保證設置的
key在子節(jié)點上不會丟失? - 如何處理
key的自動續(xù)期
以上就是基于PHP+Redis實現(xiàn)分布式鎖的詳細內(nèi)容,更多關于PHP Redis分布式鎖的資料請關注腳本之家其它相關文章!
相關文章
PHP基于cookie實現(xiàn)統(tǒng)計在線人數(shù)功能示例
這篇文章主要介紹了PHP基于cookie實現(xiàn)統(tǒng)計在線人數(shù)功能,涉及php文件讀寫、cookie訪問、計算等相關操作技巧,需要的朋友可以參考下2019-01-01
PHP5中使用mysqli的prepare操作數(shù)據(jù)庫的介紹
今天小編就為大家分享一篇關于PHP5中使用mysqli的prepare操作數(shù)據(jù)庫的介紹,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03

