webman分布式锁必须用set原子命令加锁,禁用setnx()+expire()两步法;value需拼接getmypid()与uniqid()保证唯一;解锁须用lua脚本校验value;key应含时间维度;所有实例连同一redis db。

Webman 里实现分布式锁,核心就一条:别用 setnx() + expire() 两步走,必须用 SET 原子命令加锁,否则线上必然出现死锁或误删。
为什么不能用 setnx() + expire()?
这两步非原子,中间一旦网络延迟、协程中断或进程崩溃,就会留下一个 TTL 为 -1 的 key——它永不过期,后续所有加锁请求全部失败,业务直接卡死。
- 常见错误现象:
redis-cli查到 key 存在但TTL显示-1;定时任务日志里出现多个实例同时写入同一数据 - 正确写法只有一种:
$redis->set($key, $value, ['NX', 'EX' => $ttl]) -
$value必须唯一,推荐拼接:getmypid().'_'.uniqid('', true),单靠uniqid()在多 worker 下有碰撞风险 -
$ttl要大于业务最大执行时间(含 DB 查询、第三方 API 调用),建议设为预估耗时的 2 倍
解锁必须用 Lua 脚本校验 value
直接 $redis->del($key) 是危险操作。A 拿锁后执行慢超时,Redis 自动释放;B 紧接着抢到锁;A 执行完却把 B 的锁删了——后续所有请求绕过锁,数据错乱。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 安全解锁方式:
$redis->eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $key, $value) - 如果云 Redis 禁用
EVAL(如某些阿里云社区版),只能退化为先GET再DEL,但必须把$ttl设得足够长(比如任务最长 300 秒,$ttl至少 600) - 别信“我压测几十次都没问题”——竞态窗口极小,但线上触发就是批量数据错误
定时任务去重,锁要加在逻辑块,不是命令入口
很多人把锁写在 php webman task:run --name=sync_order 的 handle() 方法开头,这完全无效:命令行启动的是全新 PHP 进程,getmypid() 和 uniqid() 不受 Webman worker 控制,不同实例生成的 $value 可能相同,锁形同虚设。
- 真正该加锁的位置,是 DB 操作前,比如
Db::table('orders')->where('status', 'pending')->update(...)之前 -
$key必须含时间维度,例如task:sync_order:20260622或task:sync_order:hour_14,避免跨天/跨时段任务互相干扰 - 所有 Webman 实例必须连同一个 Redis DB(注意不是不同 cluster 分片),否则锁根本不同步
多 worker 场景下 value 必须带 pid
Webman 默认启用多个 worker 进程,仅靠 uniqid() 无法保证全局唯一。两个不同 worker 可能生成相同 uniqid(),导致锁校验失效,A 和 B 同时进入临界区。
- 必须拼接进程标识:
$value = getmypid().'_'.uniqid('', true) - 不建议用
swoole_get_process()->pid,因为 Webman 启动时可能尚未初始化 Swoole 进程对象,getmypid()更稳定 - 若使用连接池,确保每个 worker 使用独立 Redis 连接,避免连接复用导致
$value上下文错乱
最易被忽略的点:锁的粒度和 key 设计。不是“整个任务加一把锁”,而是“每次处理一批数据前加一把锁”,key 要能区分数据范围和时间,否则今天和明天的任务会互相阻塞,或者不同商户订单被同一把锁拦住。










