Webman分布式锁必须用SET key value NX EX原子命令加锁,解锁须用Lua校验value后删除;禁用setnx+expire两步操作,否则会因非原子性导致死锁或误删。

SET 命令加锁必须原子,DEL 解锁必须校验 value,否则高并发下必然误删、重复执行或死锁——这是 Webman 分布式锁失效的根源,不是 Redis 不行,是用法错了。
Webman 里为什么不能用 setnx() + expire() 两步加锁
这两步非原子,中间一旦发生网络抖动、进程崩溃或超时中断,就会留下一个没过期时间的 key,变成“永不过期”的死锁。
常见错误现象:
-
redis-cli查到 key 存在但 TTL 显示 -1(表示无过期) - 定时任务只跑一次,却在日志里看到多个实例同时写入同一批数据
- 手动
DEL后任务立刻恢复,说明锁一直卡着没释放
正确做法只有一条:$redis->set($key, $value, ['NX', 'EX' => $ttl])。其中 $value 必须是当前 worker 唯一标识,推荐用 uniqid('', true).getmypid() 拼接,避免多 worker 冲突。
解锁必须用 Lua 脚本校验 $value 后再 DEL
直接 $redis->del($key) 是危险操作:A 拿锁后执行慢,超时自动释放;B 紧接着拿到锁;A 执行完却把 B 的锁删了——后续所有请求都绕过锁。
立即学习“PHP免费学习笔记(深入)”;
Webman 中应封装为安全的 unlock() 方法:
public function unlock($key, $value)
{
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
return $this->redis->eval($script, [$key], [$value]);
}
注意:
- 云 Redis 服务若禁用
EVAL(如部分阿里云 Redis 实例),只能退而求其次:先GET校验再DEL,但要接受极小概率误删 - 此时务必把
$ttl设得足够长(比如业务最大耗时 × 2),降低误删窗口期
Webman 定时任务去重的 key 设计陷阱
锁 key 不是越短越好,也不是固定写死,必须带时间维度和任务上下文。
错误做法:
-
task:sync_order—— 多次运行会互斥,导致第二天任务被拦住 -
php webman task:run --name=sync_order在命令 handle 入口加锁 —— 每次都是新进程,uniqid()和pid完全不可控
正确 key 示例:task:sync_order:20260522 或 task:sync_order:hour_2026052214。它保证:
- 同一时间窗口内只允许一个实例执行
- 不同时间窗口互不影响
- 所有 Webman 实例连接的是同一个 Redis DB(检查
config/redis.php配置,别连错集群分片)
原子性计数器别碰 INCR 单独用
Webman 高频场景(如秒杀库存、限流计数)若只用 $redis->incr($key),并发下会出现“超卖”:两个请求同时读到 99,各自 +1 变成 100 → 实际写了 101。
根本原因:INCR 本身是原子的,但配合 EXPIRE 就不是了。真正需要的是“带过期的原子自增”,只能靠 Lua:
local current = redis.call('incr', KEYS[1])
if current == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
return current
这个脚本必须和 SET ... NX EX 一样,作为整体执行。tinywan/limit-traffic 就是这么做的;而低版本 webman/rate-limiter 若未开启 use_lua,就会掉进这个坑。



















