基于PHP+Redis實(shí)現(xiàn)分布式鎖
一、Redis作為分布式鎖的優(yōu)勢(shì)
Redis是一個(gè)開源的、基于內(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)速度極快,適合于高頻讀寫的場(chǎng)景。
- 原子性:Redis對(duì)某些命令(如
SETNX
)提供了原子操作,還可以執(zhí)行l(wèi)ua腳本,所以確保了業(yè)務(wù)的穩(wěn)定性。 - 超時(shí)釋放:可以設(shè)置鎖的有效期,即使持有鎖的進(jìn)程崩潰,也能通過過期機(jī)制自動(dòng)釋放鎖,避免死鎖問題。
二、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ì)寫入,就算有N個(gè)并發(fā)同時(shí)在寫這個(gè)key,redis也能確保只會(huì)有一個(gè)能寫成功。 - 如何避免死鎖?
死鎖一般發(fā)生在我們的業(yè)務(wù)代碼拋出異?;蛘邎?zhí)行超時(shí),最終沒有釋放鎖從而導(dǎo)致產(chǎn)生了死鎖。這種情況我們可以通過增加一個(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); }
上面的代碼看起來沒有太大的問題,但是 $redis->expire() 一旦執(zhí)行失敗就創(chuàng)建了一個(gè)不過期的值,最終就可能導(dǎo)致產(chǎn)生死鎖,這就是為什么要保證命令執(zhí)行的原子性。
我們可以通過 $redis->eval() 方法執(zhí)行 lua腳本 來解決這個(gè)問題(我們不用關(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 方法使用詳解,官方的文檔和示例寫得有點(diǎn)打腦殼,完全沒寫腳本字符串中的 KEYS 和 ARGV 和傳遞參數(shù)的對(duì)應(yīng)關(guān)系。下面寫了一個(gè)對(duì)應(yīng)關(guān)系的例子方便大家理解:
語(yǔ)法:$redis>eval(string $script, ?array $args, ?int num_keys): mixed
參數(shù)說明:
- string $script 執(zhí)行的lua腳本字符串
- ?array $args lua腳本字符串中
KEYS
和ARGV
的對(duì)應(yīng)值,按順序?qū)?yīng)(可選值) - ?int num_keys lua腳本字符串中
KEYS
的數(shù)量,寫了幾個(gè)KEYS
就傳幾個(gè)(可選值)
官方文檔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));
輸出結(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 '加鎖成功 開始執(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文件讀寫、cookie訪問、計(jì)算等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01php版阿里大于(阿里大魚)短信發(fā)送實(shí)例詳解
這篇文章主要介紹了php版阿里大于(阿里大魚)短信發(fā)送實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了阿里大于短信發(fā)送接口的配置與使用技巧,需要的朋友可以參考下2016-11-11PHP5中使用mysqli的prepare操作數(shù)據(jù)庫(kù)的介紹
今天小編就為大家分享一篇關(guān)于PHP5中使用mysqli的prepare操作數(shù)據(jù)庫(kù)的介紹,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03