基于PHP+Redis实现分布式锁

 更新时间:2024年03月06日 10:19:45   作者:欢喜就好啊  
在高并发、分布式系统环境下,为了保证资源在同一时间只能被一个进程访问(例如数据库操作、文件读写等),分布式锁是一种常用的解决策略,本文给大家介绍了基于PHP+Redis实现分布式锁,需要的朋友可以参考下

一、Redis作为分布式锁的优势

Redis是一个开源的、基于内存的键值存储系统,它支持多种数据结构并具备持久化选项。由于其提供了原子操作(如SETNXEXPIRE等)和高性能特性,使得Redis成为实现分布式锁的理想选择:

  1. 性能优异:Redis是内存数据库,响应速度极快,适合于高频读写的场景。
  2. 原子性:Redis对某些命令(如SETNX)提供了原子操作,还可以执行lua脚本,所以确保了业务的稳定性。
  3. 超时释放:可以设置锁的有效期,即使持有锁的进程崩溃,也能通过过期机制自动释放锁,避免死锁问题。

二、PHP中使用Redis实现分布式锁的步骤与原理

前期准备

在使用分布式锁时候我们首先要考虑以下几点:

  • 如何确保锁的唯一性?
    使用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数据库时才会写入,就算有N个并发同时在写这个key,redis也能确保只会有一个能写成功。
  • 如何避免死锁?
    死锁一般发生在我们的业务代码抛出异常或者执行超时,最终没有释放锁从而导致产生了死锁。这种情况我们可以通过增加一个锁的有效期就能避免产生死锁。例如:
    • 使用redis的expire方法给对应的key设置一个有效期 expire(string $key, int $seconds, ?string $mode = NULL): Redis|bool
    • 使用lua脚本 redis.call("expire", KEYS[1], ARGV[2])
  • 如何确保redis命令执行的原子性?

要保证原子性必须要求一系列操作要么全部成功执行,要么全部不执行。举例:

$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
$result = $redis->setNx('key','val');
if ($result) {
	$redis->expire('key',30);
}

上面的代码看起来没有太大的问题,但是 $redis->expire() 一旦执行失败就创建了一个不过期的值,最终就可能导致产生死锁,这就是为什么要保证命令执行的原子性。

我们可以通过 $redis->eval() 方法执行 lua脚本 来解决这个问题(我们不用关心实现细节,这是底层的实现,只需要知道要保证 redis 命令执行的原子性用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 和传递参数的对应关系。下面写了一个对应关系的例子方便大家理解:

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

参数说明:

  • string $script 执行的lua脚本字符串
  • ?array $args lua脚本字符串中 KEYS 和 ARGV 的对应值,按顺序对应(可选值)
  • ?int num_keys lua脚本字符串中 KEYS 的数量,写了几个 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));

输出结果

以下是完整的实现代码:

  • 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
    }
    /**
     * 尝试获取锁,并在指定次数内进行重试
     *
     * @param int $maxRetries 最大重试次数,默认为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 加锁成功后执行具体的业务逻辑
        echo '加锁成功 开始执行加锁逻辑的时间:'.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();
?>

执行结果如下:

三、待优化的地方

  • 集群环境下如果主节点挂掉,如何保证设置的 key 在子节点上不会丢失?
  • 如何处理 key 的自动续期

以上就是基于PHP+Redis实现分布式锁的详细内容,更多关于PHP Redis分布式锁的资料请关注脚本之家其它相关文章!

相关文章

  • PHP 魔术变量和魔术函数详解

    PHP 魔术变量和魔术函数详解

    这篇文章主要简单介绍了PHP 魔术变量和魔术函数,以及使用示例,方便我们学习理解php魔术变量和魔术函数,有需要的小伙伴参考下吧。
    2015-02-02
  • php数组相加 array(“a”)+array(“b”)结果还是array(“a”)

    php数组相加 array(“a”)+array(“b”)结果还是array(“a”)

    同一个数组里面如果有相同的键名,则前面一个键名的值将会被覆盖(overwritten)
    2012-09-09
  • php动态读取数据清除最右边距的方法

    php动态读取数据清除最右边距的方法

    下面小编就为大家带来一篇php动态读取数据清除最右边距的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-04-04
  • PHP 登录完成后如何跳转上一访问页面

    PHP 登录完成后如何跳转上一访问页面

    访问网站页面时,有的页面需要授权才能访问,这时候就会要求用户登录,跳转到登录页面login.php,怎么实现登录后返回到刚才访问的页面
    2014-01-01
  • php中spl_autoload详解

    php中spl_autoload详解

    SPL 是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或类。 SPL autoload机制的实现是通过将函数指针autoload_func指向自己实现的具有自动装载功能的函数来实现的。
    2014-10-10
  • PHP中用hash实现的数组

    PHP中用hash实现的数组

    今天回顾学习了PHP中变量实现的方法,在浏览其源码是发现在PHP中所有的数据类型通过一个union存储。php语言是弱类型语言,其实现中通过记录变量的类型和值来实现其管理。
    2011-07-07
  • PHP中error_log()函数的使用方法

    PHP中error_log()函数的使用方法

    这篇文章主要介绍了PHP中error_log()函数的使用方法,实例分析了error_log自动生成相应的log文件的方法,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-01-01
  • PHP文件操作实例总结【文件上传、下载、分页】

    PHP文件操作实例总结【文件上传、下载、分页】

    这篇文章主要介绍了PHP文件操作,结合实例形式总结分析了php针对文件的上传、下载、分页等相关操作技巧与注意事项,需要的朋友可以参考下
    2018-12-12
  • PHP 设置MySQL连接字符集的方法

    PHP 设置MySQL连接字符集的方法

    我之前总是使用 mysql_query("SET NAMES 'utf8'"); 来设置 MySQL 的默认连接字符集;但是今天发现了一个 PHP 推荐的代替这个方法的设置 MySQL 连接字符集的函数
    2011-01-01
  • 如何在PHP中使用AES加密算法加密数据

    如何在PHP中使用AES加密算法加密数据

    这篇文章主要介绍了如何在PHP中使用AES加密算法加密数据,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06

最新评论