当前位置:首页 > PHP教程 > php高级应用 > 列表

基于PHP+Redis实现分布式锁

发布:smiling 来源: PHP粉丝网  添加日期:2024-04-20 18:33:12 浏览: 评论:0 

在高并发、分布式系统环境下,为了保证资源在同一时间只能被一个进程访问(例如数据库操作、文件读写等),分布式锁是一种常用的解决策略,本文给大家介绍了基于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命令执行的原子性?

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

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

上面的代码看起来没有太大的问题,但是 $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.  
  11. $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方法说明:

基于PHP+Redis实现分布式锁

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

输出结果

基于PHP+Redis实现分布式锁

以下是完整的实现代码:

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.  
  52.         $result = $this->redis->eval
  53.             $luaScript
  54.             [ $this->lockKey, $this->requestId, $this->expireTime ], 
  55.             1 
  56.         ); 
  57.  
  58.         return (bool)$result
  59.     } 
  60.     /** 
  61.      * 释放锁 
  62.      * @return bool 
  63.      */ 
  64.     public function releaseLock(): bool 
  65.     { 
  66.         $luaScript = <<<LUA 
  67.         if redis.call("get", KEYS[1]) == ARGV[1] then 
  68.             return redis.call("del", KEYS[1]) 
  69.         else 
  70.             return 0 
  71.         end 
  72. LUA; 
  73.  
  74.         $result = $this->redis->eval
  75.             $luaScript
  76.             [ $this->lockKey, $this->requestId ], 
  77.             1 
  78.         ); 
  79.  
  80.         return (bool)$result
  81.     } 
  82. ?> 

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. task(); 
  21. ?> 

执行结果如下:

基于PHP+Redis实现分布式锁

三、待优化的地方

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

如何处理 key 的自动续期。

Tags: PHP+Redis分布式锁

分享到: