PHP商城秒杀防超卖方案总结
在PHP中实现商城秒杀并防止超卖,通常可以采用以下几种方案:
1. 数据库乐观锁(版本号/条件更新)
-
原理:通过数据库的原子操作(如
UPDATE语句的条件判断)确保库存不会超卖。 -
实现:
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock > 0; -
优点:实现简单,依赖数据库事务。
-
缺点:高并发时数据库压力大,需结合重试机制。
2. Redis原子操作
-
原理:利用Redis单线程和原子命令(如
DECR/INCR)扣减库存。 -
实现:
$redis = new Redis(); $stockKey = "product:123:stock"; // 使用Lua脚本确保原子性 $script = ' local stock = tonumber(redis.call("GET", KEYS[1])) if stock > 0 then redis.call("DECR", KEYS[1]) return 1 end return 0 '; $result = $redis->eval($script, [$stockKey], 1); if ($result) { // 扣减成功,生成订单 } -
优点:高性能,适合高并发。
-
缺点:需保证Redis与数据库的数据一致性。
3. 消息队列异步处理
-
原理:将请求放入队列,异步处理订单和库存扣减。
-
实现:
-
用户请求先写入队列(如Redis List/RabbitMQ)。
-
后台Worker顺序处理队列,确保单线程扣减库存。
-
-
优点:削峰填谷,缓解数据库压力。
-
缺点:用户需等待处理结果,实时性较差。
4. 分布式锁
-
原理:使用分布式锁(如Redis的
SETNX或RedLock)控制并发。 -
实现:
$lockKey = "product:123:lock"; $token = uniqid(); // 尝试获取锁(设置过期时间防死锁) if ($redis->set($lockKey, $token, ['NX', 'EX' => 10])) { try { // 扣减库存逻辑 } finally { // 释放锁(Lua脚本保证原子性) $script = ' if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) end return 0 '; $redis->eval($script, [$lockKey, $token], 1); } } -
优点:防止分布式环境下的并发问题。
-
缺点:实现复杂,锁超时时间需合理设置。
5. 预扣库存与超时释放
-
原理:用户下单时预占库存,超时未支付则释放。
-
实现:
-
用户秒杀成功时,生成预订单并设置有效期(如30分钟)。
-
使用Redis的
EXPIRE自动释放库存,或定时任务回滚超时订单。
-
-
优点:避免库存长期被占用。
-
缺点:需处理订单状态同步。
6. 限流与降级
-
原理:控制请求流量,避免系统崩溃。
-
实现:
-
限流:使用令牌桶/漏桶算法(如Redis计数器)。
-
降级:前端添加验证码、答题环节,或直接拒绝多余请求。
-
-
示例:
$rateLimitKey = "product:123:rate_limit"; $current = $redis->incr($rateLimitKey); if ($current > 1000) { // 返回“活动太火爆”提示 }
7. 分桶库存
-
原理:将库存分散到多个Key,减少单个Key的竞争。
-
实现:
-
将库存分为10个桶:
product:123:stock_1到product:123:stock_10。 -
用户随机选择一个桶进行扣减。
-
-
优点:提高并发性能。
-
缺点:库存分配可能不均。
总结建议
-
推荐组合方案:
-
Redis + Lua脚本:处理瞬时高并发库存扣减。
-
消息队列:异步生成订单,降低数据库压力。
-
限流与降级:保护系统不被流量冲垮。
-
预扣库存:结合超时释放提升用户体验。
-
-
注意事项:
-
预热数据:活动开始前将库存加载到Redis。
-
数据一致性:通过定时任务或消息队列同步Redis与数据库。
-
幂等性:确保用户不能重复提交(如用Redis记录用户购买状态)。
-

浙公网安备 33010602011771号