基于PHP+Redis实现分布式锁

2024-04-19 0 883
目录
  • 一、Redis作为分布式锁的优势
  • 二、PHP中使用Redis实现分布式锁的步骤与原理
  • 三、待优化的地方

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

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

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

    前期准备

    • 运行环境:php 7.3.4+phpredis扩展 4.3.0+redis windows客户端 3.2.100
    • phpredis扩展文档
    • 简单了解lua脚本

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

    • 如何确保锁的唯一性?使用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 $argslua脚本字符串中KEYS和ARGV的对应值,按顺序对应(可选值)
    • ?int num_keyslua脚本字符串中KEYS的数量,写了几个KEYS就传几个(可选值)

    官方文档eval方法说明:

    基于PHP+Redis实现分布式锁

    //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));

    输出结果

    基于PHP+Redis实现分布式锁

    以下是完整的实现代码:

    • 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();
    ?>

    执行结果如下:

    基于PHP+Redis实现分布式锁

    三、待优化的地方

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

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

    您可能感兴趣的文章:

    • PHP实现Redis分布式锁的示例代码
    • PHP使用redis实现分布式锁的示例详解
    • php基于redis的分布式锁实例详解
    • php redis setnx分布式锁简单原理解析

    收藏 (0) 打赏

    感谢您的支持,我会继续努力的!

    打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
    点赞 (0)

    悠久资源 PHP 基于PHP+Redis实现分布式锁 https://www.u-9.cn/biancheng/php/188311.html

    常见问题

    相关文章

    发表评论
    暂无评论
    官方客服团队

    为您解决烦忧 - 24小时在线 专业服务