不能先rpop再查mysql库存,因为rpop虽原子但整条链路非原子,mysql事务失败会导致id丢失漏单,且并发时易超卖;正确做法是用lua脚本将库存校验与出队绑定,或队列存结构化快照数据。

为什么不能先rPop再查MySQL库存
因为rPop是原子的,但“取ID→查MySQL→扣减→写订单”整条链路不是。一旦MySQL事务失败(比如唯一索引冲突、主从延迟导致查到旧库存),商品ID已出队却没生成订单,这个ID就永远丢失——库存漏单,不可逆。
常见错误写法:
if ($redis->rPop('goods:queue:1001')) {
$stock = $pdo->query("SELECT num FROM stock WHERE id = 1001")->fetchColumn();
if ($stock > 0) {
$pdo->exec("UPDATE stock SET num = num - 1 WHERE id = 1001");
// … 写订单
}
}
问题在于:两个请求同时rPop拿到同一个ID,都查到num = 1,然后都执行成功,超卖。
- 正确路径必须把库存校验压进Redis端,且和出队动作绑定
- 要么用Lua脚本封装
decr+exists+lRem,返回值直接决定是否继续 - 要么队列里存结构化数据,如
{"goods_id":1001,"stock_snapshot":5},消费时比对快照值
Webman里调用Redis Lua脚本的硬性要求
Webman默认不启用Lua支持,eval会退化成多次网络往返,原子性失效。不配use_lua => true,脚本写得再严谨也没用。
检查config/redis.php中是否有:
'use_lua' => true,
调用时注意:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
KEYS数组传Redis key(如['seckill:stock:1001', 'user:participated:1001:123']) -
ARGV数组传参数(如[1, 'token_abc']),顺序错一个,脚本里KEYS[1]就取不到 - 别在脚本里用
redis.log(),Webman的Redis扩展不支持 - 返回整数(如
1成功、-1库存不足),避免JSON解析开销
多Worker下如何避免重复消费同一商品
Webman默认启动多个Worker进程,如果每个都定时rPop,必然抢同一队列元素。这不是Redis问题,是调度逻辑缺陷。
唯一可靠解法是加分布式锁,且必须带value校验:
- 用
SET key value EX 10 NX尝试获取锁,value设为当前Worker PID或随机UUID - 解锁必须走Lua脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 queue:lock:1001 abc123 - 禁用
SETNX + DEL两步操作,否则A进程删掉B进程的锁 - 别用
flock(),Webman跑在Swoole协程下,文件锁完全失效
缓存Key设计不当会直接引发线上事故
写死Cache::set('goods_list', $data)看着省事,实际等于埋雷:改个分页参数、换种筛选条件,缓存就永远不更新;测试/生产共用Redis,key一撞,数据全串。
正确结构必须包含三要素:
- 业务域前缀(如
seckill:goods:) - 参数签名(用
md5(json_encode([$goods_id, $user_id])),不用serialize()防PHP升级hash变化) - 版本号(如
:v3),上线即生效,比DEL seckill:goods:*更可控
敏感字段如$user_id别明文拼进key,防缓存探测和越权访问——用哈希或加密后拼接。










