ThinkPHP如何做商城秒杀_redis预扣库存与异步下单【指南】
ThinkPHP商城秒杀通过RedisList预扣库存实现原子防超卖,异步下单将订单生成交由队列处理以避免PHP阻塞。同时,利用任务ID与Hash状态监控确保数据一致性,超时未完成则自动回滚库存,保障系统最终一致性。
秒杀系统最怕什么?不是流量大,而是库存超卖。这个问题一旦发生,轻则赔钱,重则平台信誉受损。先梳理几个关键点:预扣库存、异步下单,以及这两者之间的数据一致性,缺一不可。

预扣库存:用Redis List模拟“库存池”
核心思路其实很朴素:把库存数量转成一个个具体的、可消耗的原子单位。比如100件商品,就往Redis列表里塞100个占位符。用户来抢购时,直接从列表里弹出一个元素——弹出成功,说明抢到了;弹不出来,说明库存已空。整个过程是原子的,天然防超卖,不需要额外加锁。
- 初始化库存时,用
LPUSH批量写入,比如:LPUSH goods:1001 1 1 1 ...(共100次) - 抢购时执行
$redis->lPop('goods:1001'),返回非null即表示抢到资格 - 注意:这个操作本身是原子的,不需要再考虑加锁问题
异步下单:把订单生成交给后台队列处理
前端只负责快速响应“抢到了”这件事。真正创建订单、扣减数据库库存、发送通知这些耗时操作,全部交给后台的消费者进程去处理。这样既能避免PHP请求阻塞,也能防止MySQL瞬间被冲垮。
- 抢到后立即写入任务队列:
$redis->lPush('seckill_orders', json_encode(['user_id'=>123, 'sku_id'=>1001])) - 用独立脚本(比如ThinkPHP的命令行)持续监听并消费:
BRPOP seckill_orders 0 - 消费者拿到数据后,再做幂等校验、生成正式订单、更新MySQL库存、记录日志
TP5/6中整合Redis的实用要点
ThinkPHP本身不内置Redis连接管理,需要手动封装或借助扩展。关键在于确保连接复用、错误兜底、以及与业务逻辑解耦。
- 推荐使用
think-redis或mikkle/tp-redis这类成熟扩展,避免每次new Redis对象 - 在服务层统一管理Redis实例,比如定义
SeckillService类,封装tryLockStock()和pushOrderTask()两个方法 - 务必设置超时和重试机制——
lPop失败时返回友好提示,不要抛未捕获异常
补单与一致性保障
预扣成功但后续下单失败怎么办?比如MySQL写入异常、网络中断,如果不管,库存就会“悬空”,永远无法释放。这才是整个方案里最容易出问题的地方。
解决方案是为每个预扣动作生成唯一任务ID,存入Hash结构:HSET seckill:task:xxx status "pending" user_id 123 sku_id 1001。消费者处理完成后,更新状态为"success"或"failed"。同时,定时任务扫描超时"pending"的任务——如果超过5分钟还没更新,就自动回滚:把占位符重新LPUSH回库存列表。这样,即使出现异常,系统也能自我修复,保证最终一致性。


































