欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于PHP+Redis實(shí)現(xiàn)分布式鎖

 更新時(shí)間:2024年03月06日 10:19:45   作者:歡喜就好啊  
在高并發(fā)、分布式系統(tǒng)環(huán)境下,為了保證資源在同一時(shí)間只能被一個(gè)進(jìn)程訪問(wèn)(例如數(shù)據(jù)庫(kù)操作、文件讀寫(xiě)等),分布式鎖是一種常用的解決策略,本文給大家介紹了基于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)。由于其提供了原子操作(如SETNXEXPIRE等)和高性能特性,使得Redis成為實(shí)現(xiàn)分布式鎖的理想選擇:

  1. 性能優(yōu)異:Redis是內(nèi)存數(shù)據(jù)庫(kù),響應(yīng)速度極快,適合于高頻讀寫(xiě)的場(chǎng)景。
  2. 原子性:Redis對(duì)某些命令(如SETNX)提供了原子操作,還可以執(zhí)行l(wèi)ua腳本,所以確保了業(yè)務(wù)的穩(wěn)定性。
  3. 超時(shí)釋放:可以設(shè)置鎖的有效期,即使持有鎖的進(jìn)程崩潰,也能通過(guò)過(guò)期機(jī)制自動(dòng)釋放鎖,避免死鎖問(wèn)題。

二、PHP中使用Redis實(shí)現(xiàn)分布式鎖的步驟與原理

前期準(zhǔn)備

在使用分布式鎖時(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)文章

最新評(píng)論