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

//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));
輸出結(jié)果

以下是完整的實(shí)現(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(); // 生成唯一請(qǐng)求ID
}
/**
* 嘗試獲取鎖,并在指定次數(shù)內(nèi)進(jìn)行重試
*
* @param int $maxRetries 最大重試次數(shù),默認(rèn)為3次
* @param int $retryDelay 兩次重試之間的延遲時(shí)間(單位:毫秒)
* @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;
}
/**
* 進(jìn)行加鎖
* @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è)務(wù)邏輯
echo '加鎖成功 開(kāi)始執(zhí)行加鎖邏輯的時(shí)間:'.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í)行結(jié)果如下:

三、待優(yōu)化的地方
- 集群環(huán)境下如果主節(jié)點(diǎn)掛掉,如何保證設(shè)置的
key在子節(jié)點(diǎn)上不會(huì)丟失? - 如何處理
key的自動(dòng)續(xù)期
以上就是基于PHP+Redis實(shí)現(xiàn)分布式鎖的詳細(xì)內(nèi)容,更多關(guān)于PHP Redis分布式鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
PHP基于cookie實(shí)現(xiàn)統(tǒng)計(jì)在線人數(shù)功能示例
這篇文章主要介紹了PHP基于cookie實(shí)現(xiàn)統(tǒng)計(jì)在線人數(shù)功能,涉及php文件讀寫(xiě)、cookie訪問(wèn)、計(jì)算等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01
php版阿里大于(阿里大魚(yú))短信發(fā)送實(shí)例詳解
這篇文章主要介紹了php版阿里大于(阿里大魚(yú))短信發(fā)送實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了阿里大于短信發(fā)送接口的配置與使用技巧,需要的朋友可以參考下2016-11-11
PHP中大于2038年時(shí)間戳的問(wèn)題處理方案
這篇文章主要介紹了PHP中大于2038年時(shí)間戳的問(wèn)題處理方案,需要的朋友可以參考下2015-03-03
PHP5中使用mysqli的prepare操作數(shù)據(jù)庫(kù)的介紹
今天小編就為大家分享一篇關(guān)于PHP5中使用mysqli的prepare操作數(shù)據(jù)庫(kù)的介紹,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03

