不能直接lpop再查库存,因为“取出来→写mysql→返回结果”整条链路非原子,mysql写入失败会导致商品id已出队但订单未生成,造成库存永久丢失(漏单);必须将库存校验前置,用redis lua脚本或结构化队列+快照比对确保扣减原子性。

Webman里用Redis队列做秒杀,为什么不能直接lPop再查库存?
因为lPop本身是原子操作,但“取出来→写MySQL→返回结果”这整条链路不是。一旦MySQL写入失败(如唯一索引冲突、网络超时、主从延迟),商品ID已出队却没生成订单,库存就永久丢失——这就是典型的「漏单」。
正确做法是把库存校验压进队列消费前的前置检查环节:
- 秒杀开始前,用
lPush预埋goods:queue:{activity_id},每个元素是商品ID或结构化JSON(含SKU、价格等) - 用户请求到达时,先
llen查剩余队列长度,小于等于0直接返回“已抢完” - 再
lIndex随机取一个元素做轻量校验(比如检查该SKU是否还在售),避免盲目出队 - 最后才
rPop真正消费,且必须搭配MySQL事务或Redis Lua脚本完成扣减+落库
Redis队列 + MySQL订单怎么保证不超卖?
关键在「出队」和「扣库存」不能拆成两步。常见错误是:先rPop拿到商品ID,再去MySQL执行UPDATE stock SET num = num - 1 WHERE id = ? AND num > 0——这个WHERE条件只能防负数,但无法阻止两个请求同时读到num=1,然后都执行成功。
必须让库存扣减动作具备原子性:
- 方案一(推荐):用Redis Lua脚本封装
decr+exists+lRem,返回值直接决定是否继续走MySQL建单流程 - 方案二:把商品ID和当前库存快照一起存入队列(如
["goods_id":1001,"stock":5]),消费时用HINCRBY更新Redis中的实时库存hash,再比对快照值 - 绝对不要在PHP里用
if ($stock > 0) { $redis->decr('stock'); }——中间存在毫秒级竞争窗口
Webman多Worker下,Redis队列如何避免重复消费?
Webman默认启多个Worker进程,如果每个Worker都起一个定时器去rPop,必然导致同一商品被多个Worker争抢消费。这不是Redis问题,而是任务调度逻辑缺陷。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
解决方式只有两种,且必须二选一:
- 用Redis分布式锁控制消费入口:所有Worker在执行
rPop前,先SETNX queue:lock:{activity_id} {worker_pid},并设置过期时间;获取锁成功的Worker才允许消费,完成后用Lua脚本安全删除锁 - 改用单点消费者模式:起一个独立
process(非Worker),由它独占消费队列,处理完后通过Redis::publish()通知各Worker广播结果,避免Worker间状态耦合 - 别试图用
BRPOP阻塞式监听——Webman的协程模型下,BRPOP会挂起整个协程,影响其他连接处理
为什么秒杀接口响应快了,但Redis内存暴涨?
因为大量短生命周期的队列key没清理,比如goods:queue:20260523活动结束后,只删了队列内容,没删key本身。Redis的DEL命令虽快,但若用KEYS goods:queue:*遍历再删,会阻塞主线程,高并发下直接拖垮服务。
预防措施要前置:
- 创建队列时强制带TTL:
lPush后立刻EXPIRE goods:queue:{activity_id} 86400 - 用
SCAN代替KEYS做批量清理,每次扫100个key,加sleep(1)错峰执行 - 关键点:Webman里所有Redis操作必须走连接池(
webman/redis),禁止在onMessage里new Redis()——否则连接数爆炸,TIME_WAIT堆积,Redis服务端拒绝新连接
实际部署时最容易忽略的是:活动结束信号怎么可靠触达消费者。别依赖前端倒计时或客户端上报,得用Redis的PUB/SUB或数据库状态轮询,确保队列停摆和资源释放完全同步。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










