解决秒杀高并发的一些方案
秒杀场景的特点:
- 瞬时高并发:大量请求在短时间内涌入
- 库存有限:必须避免超卖
- 一致性要求高:库存扣减和订单生成要保证正确
常见的几种实现方式:
一、Redis 方案(基于内存,效率高)
1. 库存预热队列
活动前把商品库存预先放入 Redis 队列,抢购时依次弹出。
// 预热库存
for ($i=0; $i<$count; $i++) {$redis->lpush('goods_store', 1);
}// 抢购时
$count = $redis->lpop('goods_store');
if (!$count) {insertLog('error:no store redis');return;
}
-
✅ 优点:
- FIFO 保证“先到先得”,不会超卖
- 实现简单直观
-
⚠️ 缺点:
- 大量库存预热占用内存
- Redis 宕机可能导致数据丢失
- 需要定期与数据库对账,保持一致性
2. 分布式锁 (setnx
)
do {$res = $redis->setnx("numKey", 1);$this->timeout -= 100;usleep(100);
} while ($res == 0 && $this->timeout > 0);if ($res == 0) {echo 'fail1';
} else {$num = $redis->get('num');if ($num > 0) {$redis->decr('num');$res = $redis->lPush('result', $num);echo $res ? "success:".$num : "fail2";} else {echo "fail3";}$redis->del("numKey");
}
-
✅ 优点:
- 保证并发情况下操作的互斥性
-
⚠️ 缺点:
setnx
需搭配expire
,否则进程异常会死锁- 自旋锁 +
usleep
性能浪费 - 建议用
SET key value NX EX ttl
或 RedLock 替代
3. 原子计数 (decr
)
$retNum = $redis->decr('num');
if ($retNum >= 0) {// success
} else {// fail
}
-
✅ 优点:
- Redis 原子操作,性能极高
- 实现最简洁
-
⚠️ 缺点:
- 仍需防止
retNum == -1
的情况(超卖) - Redis 数据和 MySQL 数据需异步对账
- 仍需防止
4. 乐观锁 (watch
)
$num = $redis->get('num');
if ($num > 0) {$redis->watch('num');$res = $redis->multi()->decr('num')->lPush('result', $num)->exec();echo $res ? "success:".$num : "fail1";
} else {echo "fail2";
}
-
✅ 优点:
- CAS 思路,避免加锁
-
⚠️ 缺点:
watch
到exec
之间容易冲突,失败率高- 高并发下需大量重试,性能差
二、MySQL 方案(简单,但效率差)
1. 悲观锁
SELECT stock
FROM goods
WHERE id = [商品ID]
FOR UPDATE;
-
✅ 优点:
- 保证强一致性,逻辑简单
-
⚠️ 缺点:
- 高并发下阻塞严重,吞吐量极低
- 一般只用于小规模并发或后台
2. 乐观锁(版本号 / 时间戳)
UPDATE goods
SET stock = stock - 1, version = version + 1
WHERE id = [商品ID] AND stock > 0 AND version = $currentVersion;
-
✅ 优点:
- 非阻塞,性能比悲观锁好
- 并发下保证库存不超卖
-
⚠️ 缺点:
- 更新失败需业务层重试
- 用户体验可能不佳(抢到也可能失败)
三、文件锁(不推荐)
1. 排他锁
$fp = fopen("lock.txt", "w+");
if (!flock($fp, LOCK_EX | LOCK_NB)) {echo "系统繁忙,请稍后再试";return;
}
-
✅ 优点:
- 实现简单,测试方便
-
⚠️ 缺点:
- 仅适合单机环境
- IO 开销大,性能差
- 生产环境基本不用
四、消息队列削峰(MQ)
思路:前端请求先进入 MQ(Kafka / RabbitMQ / RocketMQ),消费者再按顺序从队列中取出请求处理。
-
✅ 优点
- 削峰填谷,抵御流量洪峰
- 系统解耦,请求不会直接打爆数据库
- 易于扩展(消费者可水平扩展)
-
⚠️ 缺点
- 处理有一定延迟(最终一致性)
- 消息丢失/重复需要保证幂等性
👉 实际上,Redis 扣减库存 + MQ 异步下单 是大厂标配方案。
五、令牌桶 / 漏桶限流
思路:在入口层(Nginx / API Gateway)加限流,比如 每秒只发放 100 个令牌,拿到令牌的请求才能继续执行。
-
✅ 优点
- 限制瞬时并发,保护下游服务
- 拒绝多余请求,避免系统雪崩
-
⚠️ 缺点
- 抢不到令牌的用户直接失败(但符合秒杀场景特点)
六、预扣库存(缓存 + 数据库双写)
思路:活动开始前,将库存同步到 Redis,Redis 负责实时扣减,后台定时把结果异步写回 MySQL。
-
✅ 优点
- Redis 抗高并发,性能优
- MySQL 保存最终结果,保证持久化
-
⚠️ 缺点
- 需要处理 Redis 与 MySQL 不一致问题
- 可能出现超卖或少卖,需要补偿机制
七、用户排队(异步处理)
思路:先让用户进入排队系统(类似 12306 / JD 抢购),后台分批处理订单请求。
-
✅ 优点
- 用户体验较好,有“排队感知”
- 系统压力均匀,不会瞬间打爆
-
⚠️ 缺点
- 实现复杂,需要额外的排队服务
- 用户下单延迟高
八、静态页面 + 前端限流
思路:活动页提前生成静态页面,减少数据库和应用层压力。前端在秒杀按钮处增加限流逻辑(例如按钮灰化、随机延迟)。
-
✅ 优点
- 最大化减少应用层压力
- 提前过滤一部分无效请求
-
⚠️ 缺点
- 不适合高安全性场景(前端逻辑容易被绕过)
- 只能作为“辅助手段”
九、CDN 缓存 + 本地化校验
思路:把活动页、秒杀时间、商品信息放到 CDN,本地浏览器校验秒杀开始时间,减少集中请求的瞬时流量。
-
✅ 优点
- 降低源站压力
-
⚠️ 缺点
- 无法替代库存扣减逻辑,只能减少部分请求
十、硬件层面的支持
- 内核调优(Linux TCP 参数、文件句柄数等)
- 负载均衡(Nginx / LVS)
- 分库分表 / 读写分离
- 热点数据隔离(比如单独 Redis 实例处理秒杀库存)
总结与推荐实践
- Redis 原子操作(
decr
或 队列) → 负责扣减库存 + 限流,保证不超卖 - 消息队列(Kafka/RabbitMQ) → 负责削峰填谷,把订单请求异步落库
- MySQL 乐观锁 → 负责最终一致性,防止异常情况导致超卖
- 消息队列削峰(Kafka / RabbitMQ)
- 入口限流(令牌桶 / 漏桶)
- 排队系统(用户体验层面)
- CDN + 静态化(减少请求风暴)
- 系统层面优化(分库分表 / 负载均衡 / 内核优化)
额外注意事项:
- 接口限流(令牌桶/漏桶)
- 防刷机制(限制 IP/用户频率)
- 幂等性(同一用户不能重复下单)
- Redis 与 MySQL 数据定期对账
最终架构推荐: Redis + MQ + MySQL 组合拳,既能抗高并发,又能保证数据安全。