php秒杀超卖的根本原因是库存操作非原子性,需用redis+lua实现“读-判-改”原子化,并配合前置限流、用户拦截和db兜底校验三道防线。

PHP 秒杀活动扛不住高并发,根本不是 PHP 本身的问题,而是“查库存 → 判断 → 扣库存 → 写订单”这套线性流程在并发下天然断裂。只要这四步没被收束成原子操作,超卖就一定会发生——哪怕你用了 SELECT ... FOR UPDATE,锁竞争也会把数据库拖垮。
为什么 MySQL 的 SELECT FOR UPDATE 在秒杀里容易失效
很多人以为加了 FOR UPDATE 就万事大吉,但实际线上常踩几个坑:
- 事务没显式开启或提前提交,
FOR UPDATE变成空转,锁根本没生效 - WHERE 条件没走索引,导致行锁升级为表锁,整个商品表被堵死
- 事务持有时间过长(比如扣库存后还去调用微信支付接口),锁住资源几十毫秒,QPS 直接腰斩
- PHP-FPM 是多进程模型,每个请求独占数据库连接,无法共享锁状态,不同进程之间仍会互相绕过
更关键的是:秒杀峰值动辄几万 QPS,MySQL 单实例撑不过 2000 TPS 的 UPDATE ... WHERE stock > 0,连接池先爆,再谈锁也没用。
Redis + Lua 脚本是 PHP 秒杀的底线方案
把库存判断和扣减下沉到 Redis,用 Lua 保证“读-判-改”原子性,这是目前 PHP 生产环境最稳的路径。别用多个 GET + DECR 分步调用——网络往返间隙就是超卖窗口。
一个可用的 Lua 脚本示例(PHP 中通过 eval 调用):
if redis.call('EXISTS', KEYS[1]) == 0 then
return -2 -- 库存 key 不存在
end
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock <p>注意点:</p>
-
KEYS[1]必须是预热好的库存 key(如seckill:stock:1001),不能拼接用户 ID 或时间戳造成 key 爆炸 - 脚本返回值要检查:
-1表示库存不足,-2表示未预热,其他正数才是成功扣减后的剩余量 - PHP 调用时必须用
$redis->eval($script, [$key], 1),第二个参数是 key 数量,写错会导致脚本找不到KEYS[1]
PHP 层必须配合的三道防线
光靠 Redis 不够。PHP 进程层得主动过滤、限流、降级,否则 Redis 也会被击穿:
- 前置阀门:用
redis.incr+expire做整点抢购计数,比如seckill:gate:1001:2026042314设 TTL 60 秒,超过阈值(如 5000)直接返回503 Service Unavailable - 用户维度拦截:下单前查
seckill:user:1001:{uid}是否存在,防止同一用户重复提交(Lua 脚本里加这句成本太高,PHP 层做更轻量) - 兜底校验:Redis 扣减成功后,仍需异步落库;订单创建时再查一次 DB 库存,若发现不一致(说明 Redis 挂过或未同步),则取消该订单并告警,而不是强一致性阻塞
最后提醒一句:所有库存 key 必须在秒杀开始前由运营系统统一预热进 Redis,且初始值与 DB 严格一致。漏掉这步,Lua 脚本里的 EXISTS 判断就会永远返回 -2,整个活动直接“静默失败”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











