亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

 更新時(shí)間:2024年03月06日 10:19:45   作者:歡喜就好啊  
在高并發(fā)、分布式系統(tǒng)環(huán)境下,為了保證資源在同一時(shí)間只能被一個(gè)進(jìn)程訪問(例如數(shù)據(jù)庫操作、文件讀寫等),分布式鎖是一種常用的解決策略,本文給大家介紹了基于PHP+Redis實(shí)現(xiàn)分布式鎖,需要的朋友可以參考下

一、Redis作為分布式鎖的優(yōu)勢

Redis是一個(gè)開源的、基于內(nèi)存的鍵值存儲系統(tǒng),它支持多種數(shù)據(jù)結(jié)構(gòu)并具備持久化選項(xiàng)。由于其提供了原子操作(如SETNX、EXPIRE等)和高性能特性,使得Redis成為實(shí)現(xiàn)分布式鎖的理想選擇:

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

二、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ù)庫時(shí)才會寫入,就算有N個(gè)并發(fā)同時(shí)在寫這個(gè)key,redis也能確保只會有一個(gè)能寫成功。
  • 如何避免死鎖?
    死鎖一般發(fā)生在我們的業(yè)務(wù)代碼拋出異?;蛘邎?zhí)行超時(shí),最終沒有釋放鎖從而導(dǎo)致產(chǎn)生了死鎖。這種情況我們可以通過增加一個(gè)鎖的有效期就能避免產(chǎn)生死鎖。例如:
    • 使用redis的expire方法給對應(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ù)的對應(yīng)關(guān)系。下面寫了一個(gè)對應(yīng)關(guān)系的例子方便大家理解:

語法:$redis>eval(string $script, ?array $args, ?int num_keys): mixed

參數(shù)說明:

  • string $script 執(zhí)行的lua腳本字符串
  • ?array $args lua腳本字符串中 KEYS 和 ARGV 的對應(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(); // 生成唯一請求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)上不會丟失?
  • 如何處理 key 的自動續(xù)期

以上就是基于PHP+Redis實(shí)現(xiàn)分布式鎖的詳細(xì)內(nèi)容,更多關(guān)于PHP Redis分布式鎖的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論