本文目录导读:

- 核心原则
- 方法一:使用
set命令 + Lua 脚本(最推荐) - 方法二:使用 Predis 或 phpredis 扩展的
set()方法 - 方法三:使用 Redlock 算法(适用于高可靠场景)
- 常见问题与注意事项
在PHP项目中使用Redis分布式锁,主要用来解决高并发场景下的资源竞争问题(如秒杀、抢红包、防止重复提交等)。
以下是几种常见、规范的实现方式,从简单到可靠逐步说明。
核心原则
一个可靠的分布式锁需要满足:
- 互斥性:任何时刻只有一个客户端能持有锁。
- 防死锁:即使客户端崩溃,锁也能自动释放(设置过期时间)。
- 解铃还须系铃人:只能释放自己加的锁,不能误删别人的锁。
- 高可用:Redis本身(或集群)稳定。
使用 set 命令 + Lua 脚本(最推荐)
这是目前最通用、最被推荐的方式,避免了竞态条件。
加锁(原子操作)
利用 Redis 的 SET key value NX EX 指令,NX 保证不存在才设置(互斥),EX 设置过期时间(防死锁)。
<?php
class RedisDistributedLock {
private $redis;
private $lockKey;
private $lockValue; // 唯一标识,用于安全解锁
private $ttl; // 锁过期时间(秒)
public function __construct($redis, $lockKey, $ttl = 10) {
$this->redis = $redis;
$this->lockKey = $lockKey;
$this->ttl = $ttl;
// 生成唯一标识(可以用 UUID,或用 uniqid + 随机数)
$this->lockValue = uniqid('lock_', true);
}
/**
* 尝试获取锁
* @param int $timeout 等待超时时间(秒),0 表示不等待立即返回
* @return bool
*/
public function acquire($timeout = 0) {
$start = time();
while (true) {
// 关键:SET NX EX 原子操作
$result = $this->redis->set($this->lockKey, $this->lockValue, ['NX', 'EX' => $this->ttl]);
if ($result) {
return true;
}
// 不需要等待
if ($timeout <= 0) {
return false;
}
// 等待一段时间再重试(200ms),避免忙等待
usleep(200000); // 0.2秒
// 检查是否超时
if ((time() - $start) >= $timeout) {
return false;
}
}
}
/**
* 释放锁
* 必须使用 Lua 脚本保证原子性:先判断 value 是否自己,再删除
*/
public function release() {
// Lua 脚本:value 匹配,则删除 key
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
// 传入 KEYS 和 ARGV
return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
}
// 可选:自动续期(看门狗)—— 一般用不到,除非业务执行时间可能超过 TTL
public function renew() {
// 可以用 Lua 脚本检查 value 并重置过期时间
}
}
使用示例:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lock = new RedisDistributedLock($redis, 'my_lock_key', 10);
if ($lock->acquire(5)) { // 等待最多 5 秒
try {
// 执行临界区代码(如扣库存、写订单)
echo "执行业务逻辑...\n";
} finally {
$lock->release(); // 确保释放锁
}
} else {
echo "获取锁失败,请稍后重试\n";
}
使用 Predis 或 phpredis 扩展的 set() 方法
如果你使用 Predis 或 phpredis 扩展,写法类似,但注意参数格式。
使用 phpredis(如上例):
// 注意:phpredis 的 set 参数需要写成 ['NX', 'EX' => 10] 或 'NX', 'EX', 10 $redis->set($key, $value, ['NX', 'EX' => 10]); // 推荐 // 或者老版本 // $redis->set($key, $value, ['nx', 'ex' => 10]);
使用 Predis:
$client->set($key, $value, 'NX', 'EX', 10);
使用 Redlock 算法(适用于高可靠场景)
Redis 是主从架构,且需要严格防止锁丢失(例如主节点挂掉后,从节点还没同步就接管),可以考虑 Redlock 算法。
Redlock 需要在 多个独立的 Redis 节点 上同时加锁,只要大多数节点(N/2+1)加锁成功就算成功。
但注意:普通业务用不到 Redlock,它很重,只在金融、高一致性场景下才考虑,PHP 中可以使用封装好的库如 illuminate/redis(Laravel 的 Redis 锁)或 redlock-php。
常见问题与注意事项
-
锁超时时间(TTL)如何设定?
- 需要大于业务最大执行时间,例如业务平均 1 秒,最大可能 3 秒,TTL 建议设 5-10 秒。
- 如果业务执行可能超时,可以加一个看门狗(Watchdog) 机制:在锁持有期间,每隔 1/3 TTL 时间自动续期。
-
为什么要用唯一标识(lockValue)?
- 防止误删:如果线程A持锁超时后锁被释放,线程B拿到锁,此时线程A突然完成并调用
del,会错误删掉线程B的锁。 - 通过 Lua 脚本判断 value 确保是自己删自己的。
- 防止误删:如果线程A持锁超时后锁被释放,线程B拿到锁,此时线程A突然完成并调用
-
Redis 单点故障?
Redis 挂了,锁功能不可用,可以用 Redis 哨兵或集群提高可用性,追求高可用可以考虑 Redlock。
-
可重入锁?
- 上述实现是不可重入的(一个线程不能重复获取同一把锁),如果需要可重入,可以用
incr或维护一个 hash 结构记录持有者和重入次数。
- 上述实现是不可重入的(一个线程不能重复获取同一把锁),如果需要可重入,可以用
-
Laravel / Symfony 框架?
- Laravel 内置了基于 Redis 的锁:
Cache::lock('my_lock', 10)->get(function () { // 业务逻辑 }); - Symphony 的 Lock 组件也支持 Redis 锁。
- Laravel 内置了基于 Redis 的锁:
| 方式 | 可靠性 | 复杂度 | 推荐场景 |
|---|---|---|---|
| SET NX EX + Lua 脚本 | 高(满足大部分需求) | 低 | 通用方案,强烈推荐 |
| 纯 SET NX(无 Lua) | 中(有误删风险) | 低 | 不推荐 |
| Redlock | 非常高(跨节点) | 高 | 金融级、多节点强一致 |
对于绝大多数 PHP 项目,方法一(SET NX EX + Lua 释放锁)已经足够,简单、可靠、高效。