基于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命令执行的原子性?
要保证原子性必须要求一系列操作要么全部成功执行,要么全部不执行,举例:
- $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 的自动续期。
Tags: PHP+Redis分布式锁
- 上一篇:PHP WebSocket的技术解析与使用指南详解
- 下一篇:最后一页
推荐文章
热门文章
最新评论文章
- 写给考虑创业的年轻程序员(10)
- PHP新手上路(一)(7)
- 惹恼程序员的十件事(5)
- PHP邮件发送例子,已测试成功(5)
- 致初学者:PHP比ASP优秀的七个理由(4)
- PHP会被淘汰吗?(4)
- PHP新手上路(四)(4)
- 如何去学习PHP?(2)
- 简单入门级php分页代码(2)
- php中邮箱email 电话等格式的验证(2)