php不适合直接处理高并发秒杀,应拆分“抢资格”与“扣库存”,用redis原子操作预减库存(decr+expire)、异步队列下单、前端限流及最终一致性对账补偿。

PHP 本身不适合直接扛高并发秒杀请求,硬上会垮在 Web 层或数据库层;真正能稳住的,是把「抢资格」和「扣库存」拆开,用缓存 + 队列 + 原子操作做隔离。
为什么不能直接用 UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock > 0
这条 SQL 看似安全,但在 PHP-FPM + MySQL 默认配置下,高并发时会出现大量行锁等待、死锁或超卖:
- MySQL 的
WHERE stock > 0条件检查和SET stock = stock - 1不是原子的(除非加SELECT ... FOR UPDATE,但会严重阻塞) - PHP 进程间无法共享锁状态,靠数据库锁又扛不住 5000+ QPS
- 每次请求都查库 → Redis 缓存击穿 + MySQL 连接池打满 → 502/504 大量出现
用 Redis 实现库存预减:DECR + EXPIRE 组合必须配对
核心逻辑不是“查库存再扣”,而是“先扣再验证”,靠 Redis 原子性兜底:
- 秒杀开始前,用
SET stock:1001 100初始化库存,再EXPIRE stock:1001 3600防久未清理 - 用户请求进来,直接
DECR stock:1001—— 返回值 ≥ 0 才代表抢到了资格 - 返回负数?说明库存已扣完,立刻返回「已售罄」,不走后续流程
- 注意:
DECR对不存在的 key 会先初始化为 0 再减,所以必须提前SET,不能依赖首次DECR
异步下单队列:别让 PHP 等 MySQL INSERT 完成才返回
抢到资格 ≠ 下单成功,要解耦「资格确认」和「订单落库」:
立即学习“PHP免费学习笔记(深入)”;
- 抢成功后,只往 Redis List(如
queue:order)里LPUSH一条轻量 JSON:{"uid":123,"pid":1001,"ts":171xxxxxx} - 用独立的 PHP CLI 脚本(或 Swoole Worker)持续
BRPOP queue:order 1消费,批量写 MySQL 订单表 - 避免每个请求都触发一次
INSERT INTO orders—— 单机 MySQL 插入极限约 1k~2k TPS,而 Redis List 可轻松扛 10w+ QPS - 消费脚本需做幂等:用
uid+pid+ts拼唯一订单号,插入前先SELECT判重
前端和网关层必须做的三件事
再好的后端也架不住恶意刷请求,防护得从前端就开始:
- 按钮点击后立即置灰 + 显示「提交中」,防止用户连点(JS 层拦截)
- Nginx 层用
limit_req限制单 IP 每秒最多 5 次 /seckill 接口请求 - 接口必须带时效性签名,例如
sign=md5(uid+pid+salt+timestamp),且timestamp与服务端时间差超过 2 秒即拒掉 —— 防工具脚本重放
真正难的不是代码怎么写,而是库存数字在 Redis、MySQL、消息队列、本地缓存之间如何保持最终一致;一旦某个环节失败(比如 Redis 扣减成功但消息丢失),就得靠定时任务对账补偿 —— 这部分往往被教程跳过,但线上出问题,90% 出在这儿。











