设为首页收藏本站
天天打卡

 找回密码
 立即注册
搜索
查看: 60|回复: 15

基于PHP+Redis实现分布式锁

[复制链接]

3

主题

53

回帖

173

积分

注册会员

积分
173
发表于 2024-4-20 08:21:14 | 显示全部楼层 |阅读模式
目录


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

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

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

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

前期准备
  1. <ul><li>运行环境: [code]php 7.3.4
复制代码
+
  1. phpredis扩展 4.3.0
复制代码
+
  1. redis windows客户端 3.2.100
复制代码
  • phpredis扩展文档
  • 简单了解lua脚本
    [/code]在使用分布式锁时候我们首先要考虑以下几点:

    • 如何确保锁的唯一性?
      使用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命令执行的原子性?
    要保证原子性必须要求一系列操作要么全部成功执行,要么全部不执行。举例:
    1. $redis = new \Redis();
    2. $redis->connect('127.0.0.1',6379);
    3. $result = $redis->setNx('key','val');
    4. if ($result) {
    5.         $redis->expire('key',30);
    6. }
    复制代码
    上面的代码看起来没有太大的问题,但是 $redis->expire() 一旦执行失败就创建了一个不过期的值,最终就可能导致产生死锁,这就是为什么要保证命令执行的原子性。
    我们可以通过 $redis->eval() 方法执行 lua脚本 来解决这个问题(我们不用关心实现细节,这是底层的实现,只需要知道要保证 redis 命令执行的原子性用lua脚本就行)。示例:
    1. $redis = new \Redis();
    2. $redis->connect('127.0.0.1',6379);
    3. $luaScript = <<<LUA
    4.            if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
    5.                redis.call("expire", KEYS[1], ARGV[2])
    6.                return true
    7.            end
    8.            return false
    9. LUA;

    10. $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脚本字符串中
      1. KEYS
      复制代码
      1. ARGV
      复制代码
      的对应值,按顺序对应(可选值)

    • ?int num_keys lua脚本字符串中
      1. KEYS
      复制代码
      的数量,写了几个
      1. KEYS
      复制代码
      就传几个(可选值)

    官方文档eval方法说明:
    1. //index.php
    2. $redis = new \Redis();
    3. $redis->connect('127.0.0.1',6379);
    4.    
    5. $luaScript = <<<LUA
    6.    return {KEYS[1],KEYS[2],KEYS[3],ARGV[1],ARGV[2]};
    7. LUA;
    8. var_dump($redis->eval($luaScript,[1,2,3,4,5],3));
    复制代码
    输出结果

    以下是完整的实现代码:

    • RedisDistributedLock.php
    1. <?php
    2. class RedisDistributedLock {
    3.     private $redis;
    4.     private $lockKey;
    5.     private $requestId;
    6.     private $expireTime;
    7.     /**
    8.      * @param string $lockKey    加锁的key
    9.      * @param int    $expireTime 锁的有效期(单位:秒)
    10.      */
    11.     public function __construct(string $lockKey, $expireTime = 30)
    12.     {
    13.         $redis = new \Redis();
    14.         $redis->connect('127.0.0.1',6379);
    15.         $this->redis      = $redis;
    16.         $this->lockKey    = $lockKey;
    17.         $this->expireTime = $expireTime;
    18.         $this->requestId  = uniqid(); // 生成唯一请求ID
    19.     }
    20.     /**
    21.      * 尝试获取锁,并在指定次数内进行重试
    22.      *
    23.      * @param int $maxRetries 最大重试次数,默认为3次
    24.      * @param int $retryDelay 两次重试之间的延迟时间(单位:毫秒)
    25.      * @return bool 是否成功获取锁
    26.      */
    27.     public function acquireLock(int $maxRetries = 3, int $retryDelay = 50): bool
    28.     {
    29.         
    30.         for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
    31.             if ($this->acquireLockOnce()) {
    32.                 return true;
    33.             }
    34.             usleep($retryDelay * 1000);
    35.         }
    36.         return false;
    37.     }
    38.     /**
    39.      * 进行加锁
    40.      * @return bool 加锁是否成功
    41.      */
    42.     private function acquireLockOnce(): bool
    43.     {
    44.         $luaScript = <<<LUA
    45.             if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
    46.                 redis.call("expire", KEYS[1], ARGV[2])
    47.                 return true
    48.             end
    49.             return false
    50. LUA;

    51.         $result = $this->redis->eval(
    52.             $luaScript,
    53.             [ $this->lockKey, $this->requestId, $this->expireTime ],
    54.             1
    55.         );

    56.         return (bool)$result;
    57.     }
    58.     /**
    59.      * 释放锁
    60.      * @return bool
    61.      */
    62.     public function releaseLock(): bool
    63.     {
    64.         $luaScript = <<<LUA
    65.         if redis.call("get", KEYS[1]) == ARGV[1] then
    66.             return redis.call("del", KEYS[1])
    67.         else
    68.             return 0
    69.         end
    70. LUA;

    71.         $result = $this->redis->eval(
    72.             $luaScript,
    73.             [ $this->lockKey, $this->requestId ],
    74.             1
    75.         );

    76.         return (bool)$result;
    77.     }
    78. }
    79. ?>
    复制代码

    • index.php
    1. <?php
    2. include 'RedisDistributedLock.php';
    3. function task() {
    4.     $lockKey = 'task_1';
    5.     $handler = new RedisDistributedLock($lockKey);
    6.     $startTime = time();
    7.     if ($handler->acquireLock(4)) {
    8.         //@TODO 加锁成功后执行具体的业务逻辑
    9.         echo '加锁成功 开始执行加锁逻辑的时间:'.date('Y-m-d H:i:s',$startTime);
    10.         echo "\r\n";
    11.         echo '锁定到:'.date('Y-m-d H:i:s',time() + 15);
    12.         sleep(15);
    13.         $handler->releaseLock();
    14.         echo "\r\n";
    15.         echo '---15s后已释放锁---';
    16.     } else {
    17.         echo '加锁失败:'.date('Y-m-d H:i:s',$startTime);
    18.         return false;
    19.     }
    20. }
    21. task();
    22. ?>
    复制代码
    执行结果如下:


    三、待优化的地方


    • 集群环境下如果主节点挂掉,如何保证设置的
      1. key
      复制代码
      在子节点上不会丢失?
    • 如何处理
      1.  key
      复制代码
      的自动续期
    以上就是基于PHP+Redis实现分布式锁的详细内容,更多关于PHP Redis分布式锁的资料请关注脚本之家其它相关文章!

    免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
  • 本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    0

    主题

    46

    回帖

    92

    积分

    注册会员

    积分
    92
    发表于 2024-5-30 10:47:54 | 显示全部楼层
    已测试,非常不错

    2

    主题

    52

    回帖

    150

    积分

    注册会员

    积分
    150
    发表于 2024-5-31 23:38:27 | 显示全部楼层
    确实牛逼

    0

    主题

    53

    回帖

    107

    积分

    注册会员

    积分
    107
    发表于 2024-6-1 05:38:52 | 显示全部楼层
    说得太好了,完全同意!

    0

    主题

    45

    回帖

    91

    积分

    注册会员

    积分
    91
    发表于 2024-6-18 06:57:17 | 显示全部楼层
    我们一起努力,共同解决问题吧。

    1

    主题

    33

    回帖

    89

    积分

    注册会员

    积分
    89
    发表于 2024-6-19 12:23:51 | 显示全部楼层
    谢谢你分享这个信息

    1

    主题

    61

    回帖

    123

    积分

    注册会员

    积分
    123
    发表于 2024-7-2 19:20:02 | 显示全部楼层
    我想了解更多

    1

    主题

    52

    回帖

    128

    积分

    注册会员

    积分
    128
    发表于 2024-8-18 07:22:05 | 显示全部楼层
    这个话题很有趣,我想多了解一些
    • 打卡等级:初来乍到
    • 打卡总天数:3
    • 打卡月天数:3
    • 打卡总奖励:55
    • 最近打卡:2024-11-21 00:32:01

    2

    主题

    23

    回帖

    168

    积分

    注册会员

    积分
    168

    热心会员付费会员

    发表于 2024-8-30 22:09:24 | 显示全部楼层
    我不太确定,可能需要再确认一下。
    • 打卡等级:无名新人
    • 打卡总天数:1
    • 打卡月天数:0
    • 打卡总奖励:12
    • 最近打卡:2024-05-27 11:13:31

    2

    主题

    49

    回帖

    155

    积分

    注册会员

    积分
    155
    发表于 2024-9-11 11:38:01 | 显示全部楼层
    好用好用
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    手机版|小黑屋|爱云论坛 - d.taiji888.cn - 技术学习 免费资源分享 ( 蜀ICP备2022010826号 )|天天打卡

    GMT+8, 2024-11-24 08:44 , Processed in 0.103663 second(s), 28 queries .

    Powered by i云网络 Licensed

    © 2023-2028 正版授权

    快速回复 返回顶部 返回列表