您现在的位置是:首页 > 技术教程 正文

PHP+Redis实现分布式锁

admin 阅读: 2024-03-16
后台-插件-广告管理-内容页头部广告(手机)

在高并发、分布式系统环境下,为了保证资源在同一时间只能被一个进程访问(例如数据库操作、文件读写等),分布式锁是一种常用的解决策略。

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

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

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

二、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); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面的代码看起来没有太大的问题,但是 $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);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 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方法说明:

1

//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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 输出结果
    2

以下是完整的实现代码:

  • 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; } } ?>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
  • 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(); ?>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    执行结果如下:在这里插入图片描述

三、待优化的地方

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

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜