redis::decr不能直接用于库存扣减,因其仅保证减操作原子性,不校验余量,高并发下易超卖;必须用lua脚本将“判断+扣减”封装为原子操作,并配合redis预占位、唯一索引和异步落库实现防超发。

Webman 本身不自带分布式锁和原子扣减能力,直接用它发优惠券大概率会超发——尤其在高并发抢券场景下。必须自己补足库存校验、并发控制、幂等设计这三块,否则系统上线即翻车。
为什么 Redis::decr 不能直接当库存扣减用
很多人以为用 Redis::decr 扣库存就天然线程安全,其实漏掉了关键一环:它只保证“减操作”原子,但不保证“减之前是否还有余量”。如果多个请求同时读到库存=1,都会执行 decr,结果库存变成 -1,券被多发。
- 正确做法是用 Lua 脚本把“判断 + 扣减”打包成一个原子操作,例如:
if redis.call("get", KEYS[1]) >= ARGV[1] then return redis.call("decrby", KEYS[1], ARGV[1]) else return -1 end - Webman 中调用时注意传参顺序:
Redis::eval($script, 1, $key, $need),第一个参数是 key 数量,不是 key 名 - 别用
Redis::get+Redis::decr两步走——中间存在竞态窗口,压测时必现超发
用户领券幂等怎么靠 user_id + coupon_id 落地
光在数据库加唯一索引(user_id, coupon_id)不够,因为插入失败会抛异常,得提前拦截。Webman 的协程特性让传统“查一遍再插”不可靠,必须用 Redis 预占位。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 发券前先
Redis::setnx("coupon:used:{$user_id}:{$coupon_id}", 1),过期时间设为比业务超时长 2 秒(比如接口超时 5s,这里设 7s) - 如果
setnx返回 0,直接返回“已领取”,不查 DB;返回 1 才继续走库存校验和写库流程 - 写库成功后不用删 Redis key——靠过期自动清理;写库失败则要
Redis::del,否则用户无法重试 - 别用
user_id做 Redis key 主键(如coupon:used:{$user_id}),会导致单 key 过热,打爆 Redis 实例
Webman 的 onWorkerStart 里初始化 Redis 连接要注意什么
Webman 多进程模型下,每个 worker 进程都需要独立的 Redis 连接,但连接不能在每次请求里 new —— 会快速耗尽 Redis 连接数或触发 TIME_WAIT。
- 必须在
onWorkerStart回调中初始化Redis::connect()或Predis\Client实例,并赋值给全局静态变量(如self::$redis) - 别在
onWorkerStop里close(),Webman 不保证该回调一定执行;连接由进程退出时自动释放更稳妥 - 如果用了连接池(如
webman/redis插件),确认配置里的max_connections是按 worker 数 × 每 worker 并发量来设的,不是拍脑袋填 100 - 本地开发用
127.0.0.1,上线必须换redis://:password@host:port/0格式,否则密码和 DB 号会被忽略
真正难的不是写下发券逻辑,而是把“库存校验 → 预占位 → 写库 → 补偿”这整条链路在协程+多进程环境下做到无状态、可重入、可观测。比如 Redis 脚本执行失败时,是重试还是降级?DB 写入失败后,Redis 预占位要不要回滚?这些边界没想清楚,流量一上来最先崩的就是优惠券服务。










