正确做法是通过DI容器注入LockInterface获取RedisLock,避免手动new导致连接复用失效;扣减库存必须使用Lua脚本保证原子性,不可用PHP层三步操作。

Hyperf 里用 RedisLock 实现分布式锁,别直接 new
Hyperf 自带的 RedisLock 看起来开箱即用,但直接 new RedisLock($redis) 会绕过容器管理,导致连接复用失效、生命周期混乱,高并发下容易锁失效或连接耗尽。
正确做法是通过 DI 容器获取:
- 确保已启用
Hyperf\Redis\RedisFactory和Hyperf\Lock\LockFactory(默认已注册) - 在方法中注入
LockInterface,而非手动构造RedisLock - 锁前加
try/catch捕获LockTimeoutException,避免死等
示例:
public function buy(Item $item)
{
try {
$lock = $this->lockFactory->get('redis', 'item:' . $item->id);
if (!$lock->lock(5, 30)) { // 最多等 5s,锁持有 30s
throw new BusinessException('抢购太火爆,请稍后再试');
}
// ... 扣库存逻辑
} finally {
$lock->unlock();
}
}
扣减库存必须用 Lua 脚本,不能靠 PHP 层判断
单纯查库存 → 判断 → 扣减,三步在并发下必然超卖。Hyperf 的 Redis 客户端支持 eval,应把原子性逻辑压进 Lua。
常见错误是只用 decr,没校验结果是否 ≥ 0;或用 get+set 组合,中间被其他请求插队。
- Lua 脚本必须返回明确状态(如
1表示成功扣减,0表示库存不足) - 脚本里用
redis.call('get', KEYS[1])读,redis.call('decr', KEYS[1])写,再判断值是否>= 0 - Hyperf 中调用:
$redis->eval($script, [$key], 1),第三个参数是 key 数量
脚本示例(精简版):
if redis.call('get', KEYS[1]) == false then
return -1
end
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
锁粒度要细到商品 SKU,不是用户 ID 或订单号
用 user_id 加锁会导致同一用户无法并发买不同商品;用 order_id 加锁则根本没意义——订单还没创建。
真正需要互斥的是「对同一 SKU 的库存变更操作」,所以锁 key 必须含 sku_id:
- 推荐格式:
lock:stock:{sku_id},避免和其他业务 key 冲突 - 不要拼接动态字段如
user_id进 lock key —— 会放大锁竞争,降低吞吐 - 如果涉及多规格(如颜色+尺寸),key 必须包含完整组合标识,比如
lock:stock:{sku_id}:{spec_hash}
注意:锁 key 过长或含特殊字符时,需做 md5 或 base64url_encode 处理,否则 Redis 可能报错 ERR invalid argument。
解锁必须用 DEL + GETSET 校验,不能无脑 del
Hyperf 的 RedisLock::unlock() 默认用 DEL,但若锁已过期、被其他客户端续期或误删,直接 DEL 可能删掉别人的锁。
安全解锁必须验证锁值(随机 token)一致:
- Hyperf 2.2+ 的
RedisLock已内置 token 校验,只要没重写unlock方法就安全 - 自定义锁实现时,务必用 Lua 脚本执行「GET + DEL」原子操作,例如:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
传入的 ARGV[1] 是加锁时 set 的唯一 token,Hyperf 默认用 uniqid() 生成。
最容易被忽略的一点:锁自动续期(renew)和超时时间设置不匹配,会导致续期失败后锁提前释放——检查 Hyperf\Lock\Driver\RedisDriver 的 renewInterval 是否小于 expire 的 1/3。


















