适合谁看正在处理高并发库存扣减的后端开发者如果你只关心业务逻辑可以跳过代码部分直接看思路。前置知识熟悉 Redis 分布式锁、消息队列基本概念能看懂 PHP 伪代码。incident大促当天系统开始“卡死”2024 年黑五期间一个日淘代购站点在 1000 并发左右时订单处理开始出现明显延迟。监控显示下单接口平均响应时间从 120ms 飙升到 3.2s库存扣减接口超时率高达 15%用户端看到”库存不足”但实际还有货超卖率约 3‰当时团队以为是数据库扛不住紧急扩容了从库但问题只缓解了10分钟。真正的瓶颈藏得很深。debug逐层排查发现两个“隐形杀手”1. N1 查询100 个商品产生 301 次查询先看数据库。慢查询日志里大量重复的 SQLSELECT*FROMproductsWHEREid?;-- 执行了100次SELECT*FROMinventoryWHEREproduct_id?;-- 又执行了100次SELECT*FROMpricesWHEREproduct_id?;-- 再100次原来采购模块在生成订单时对每个商品都单独查询了库存和价格表。100 个商品就是 301 次查询。这是典型的 N1 问题但之前因为数据量小没暴露。2. 自研 Redis 锁性能抖动 锁失效再看库存扣减逻辑。自研了一套 Redis 分布式锁基于SETNX 过期时间$lockKeystock_lock_.$productId;$lockedRedis::setnx($lockKey,1);if($locked){Redis::expire($lockKey,3);// 3秒自动释放// 扣减库存$stockRedis::decr(stock_.$productId);if($stock0){// 回滚Redis::incr(stock_.$productId);}Redis::del($lockKey);}这个方案有两个致命问题锁过期导致数据不一致当扣减操作超过 3 秒比如网络抖动或 GC 停顿锁自动释放其他请求进入后读到旧库存导致超卖。性能抖动高并发下SETNX争抢锁本身就有开销而且del操作在锁被其他线程持有时会误删引发连锁反应。压测显示1000 并发下锁平均等待时间从 1ms 飙升到 50ms且约 0.5% 的请求会因锁误删而出现库存负数。root_cause选型失衡性能和一致性双双失守两个问题叠加本质是性能与一致性之间的平衡被打破。N1 查询是设计阶段的偷懒而自研锁则是过度相信”简单方案能扛住高并发”。技术选型需要在性能和可维护性之间找到平衡点而不是极端追求简单或极端追求复杂。这套系统上线前从未做过性能基准测试导致隐患一直潜伏到黑五流量高峰才暴露。当时面临的选择| 方案 | 一致性 | 性能 | 复杂度 ||||||| 自研 Redis 锁 | 弱 | 中 | 低 || Redlock | 强 | 低 | 高 || Lua 脚本 | 强 | 高 | 中 || 消息队列异步化 | 最终一致 | 极高 | 中 |自研锁在低并发下表现尚可但一旦突破阈值性能抖动和锁失效风险同时爆发。一个方案只能适应特定场景超出就崩溃。fixLua 脚本 消息队列把库存扣减变成异步事件1. 用 Lua 脚本实现原子库存扣减Redis 2.6 支持 Lua 脚本可以保证多条命令的原子性且不依赖锁-- stock_decr.lualocalkeyKEYS[1]localdecrBytonumber(ARGV[1])localstockredis.call(GET,key)ifnotstockthenreturn-1-- key不存在endstocktonumber(stock)ifstockdecrBythenreturn-2-- 库存不足endredis.call(DECRBY,key,decrBy)returnstock-decrByPHP调用$scriptfile_get_contents(stock_decr.lua);$resultRedis::eval($script,1,stock_.$productId,$quantity);if($result-2){// 库存不足进入等待队列或提示用户}这个方案消除了锁的争抢和过期问题单次操作耗时从 50ms 降到 1ms 以内。2. 引入消息队列异步处理订单库存扣减成功后不立即生成订单而是将订单数据推送到 RocketMQ由消费者异步处理。这样下单接口只做库存校验 消息推送响应时间降到 20ms消费者批量处理订单顺便解决 N1 查询用WHERE id IN (...)一次查完如果库存扣减成功但后续处理失败通过消息重试保证最终一致改造后性能基准测试数据1000 并发| 指标 | 改造前 | 改造后 |||||| 平均响应时间 | 3.2s | 45ms || 超时率 | 15% | 0% || 超卖率 | 3‰ | 0‰ || 数据库 QPS | 1200 | 180 |这个方案后来被固化到 Taocarts 的采购模块中——这是 Taocarts 中采购模块的简化实现实际生产环境还要加上失败重试和消息队列缓冲。Taocarts 的库存扣减组件内置了 Lua 脚本并提供 RocketMQ 的默认配置模板方便开发者快速接入。效果一次性能复盘推动的性能基准测试体系这次事故后建立了一套性能基准测试流程每次大促前用 JMeter 模拟 1.5 倍预期并发持续压测 30 分钟观察响应时间、错误率、CPU/内存/网络 IO。如果某个指标超过阈值自动触发告警并回滚。这套性能基准测试体系后来被固化到发布流程中所有核心接口在上线前必须通过基准测试。后续两次大促的线上事故从平均 3 次降为 0 次。更重要的是团队学会了”先做性能基准测试再上线”的工程纪律。记忆点自研 Redis 锁在高并发下不是”简单可靠”而是”简单脆弱”——性能抖动和锁失效风险是隐蔽的只有压测才能暴露。而 Lua 脚本 消息队列的异步化方案用可预期的性能换来了稳定性。