Yii2秒杀场景必须用Redis分布式锁防超卖,核心是SET NX EX原子加锁、Lua脚本校验token释放锁、Lua原子扣库存,并配合数据库乐观锁兜底。

Yii2在高并发秒杀场景下,单靠数据库事务和乐观锁很难扛住瞬时流量,Redis锁是必须引入的核心防线。它不解决所有问题,但能拦住90%以上的超卖风险——关键在于怎么用对、用稳、用得有弹性。
Redis分布式锁必须带原子性释放
很多项目直接用 setnx + expire 两步操作,这是危险的。网络延迟或进程崩溃可能导致锁未释放,形成死锁。正确做法是用 Redis 的 SET key value EX seconds NX 命令一步完成加锁,同时设置过期时间防死锁。
- Yii2中推荐写法:
$lockKey = "miaosha:lock:{$productId}"; - 执行:
Yii::$app->redis->executeCommand('SET', [$lockKey, $token, 'EX', 10, 'NX'])(10秒自动过期) - 释放锁必须用 Lua 脚本校验 token,避免误删他人锁:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
库存扣减必须走原子脚本,不能分步查+减
先 GET 再 DECR 是经典漏洞:两个请求同时读到库存=1,都执行减1,结果变成-1。必须用 Lua 把“判断+扣减”封装成原子操作。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 示例脚本:
local stock = redis.call("GET", KEYS[1])<br>if tonumber(stock) > tonumber(ARGV[1]) then<br> return redis.call("DECRBY", KEYS[1], ARGV[1])<br>else<br> return -1<br>end - Yii2调用:
Yii::$app->redis->eval($script, 1, $stockKey, $buyCount) - 返回值为负数即库存不足,直接中断流程,不进数据库
锁粒度要细,但别锁太碎
锁整个商品ID是常见做法,但如果一个商品支持多规格(如颜色/尺寸),锁粒度太粗会降低并发;锁到SKU级又可能因key过多增加Redis压力。折中方案是按「商品+规格组合」生成锁键,例如:"lock:product_123:sku_456"。
- 避免全局锁(如
lock:all),它会让所有请求排队 - 也不要为每个用户加锁(如
lock:user_789:product_123),无意义放大锁数量 - 锁有效期建议 5–10 秒,足够完成扣库存+写订单消息,又不至于长期占位
兜底必须有,Redis挂了也不能超卖
Redis只是缓存层,不是唯一真相。一旦Redis不可用,系统应自动降级到数据库乐观锁兜底:
- 订单创建前,仍执行一次
UPDATE product SET stock = stock - 1 WHERE id = ? AND stock >= 1 - 检查
affected_rows === 1,失败则回滚并提示“库存已抢完” - 配合数据库
version字段做乐观控制,防止重复扣减 - Yii2中可封装为事务方法:
$transaction = Yii::$app->db->beginTransaction(); ... $transaction->commit();
不复杂但容易忽略:锁、脚本、降级这三层,缺一不可。光有Redis锁,挡不住网络抖动;光有Lua脚本,扛不住Redis宕机;光有数据库兜底,扛不住QPS破万。三者协同,才是真实抗压的底线。

















