资讯动态

电商系统超卖问题解决方案与实战优化

发布时间:2026/10/26 3:39:37 来源:尧图企业网站定制
1. 超卖问题的本质与危害第一次接触超卖这个词是在2016年做电商促销系统时。凌晨2点运营同事突然打来电话商品库存显示-1000多客户已经付款但没货可发了这就是典型的超卖场景——系统显示的库存数量与实际可售库存不一致导致卖出超过实际库存的商品。超卖问题本质上是个并发控制问题。当多个用户同时下单购买同一商品时系统如果没有做好并发控制就会出现库存扣减错误。比如商品A当前库存100件用户1和用户2同时下单各买1件两个请求几乎同时到达服务器两个线程都读取到库存为100都执行100-199最终库存被设置为99实际应该98这种问题在以下场景尤为突出秒杀活动大量用户抢购少量商品限时折扣热门商品集中下单库存较少的商品更容易被多线程同时操作超卖带来的直接后果包括客户投诉付了款却被告知没货商誉损失客户对平台失去信任赔偿成本可能需要补偿客户运营混乱需要人工核对处理异常订单注意超卖问题在测试环境很难发现因为测试时并发量通常不够大。这就是为什么很多系统上线前测试正常一到促销就出问题。2. 常见解决方案对比分析2.1 数据库乐观锁方案这是最基础的解决方案通过在更新时校验库存版本号或时间戳UPDATE products SET stock stock - 1, version version 1 WHERE id 123 AND version 5 AND stock 0优点实现简单只需要修改SQL对系统改动小缺点高并发下大量请求会失败返回影响行数为0用户体验差显示有货但下单失败需要配合重试机制适用场景并发量不大的普通商品对用户体验要求不高的后台系统2.2 数据库悲观锁方案通过SELECT...FOR UPDATE锁定记录BEGIN; SELECT * FROM products WHERE id 123 FOR UPDATE; -- 检查库存 UPDATE products SET stock stock - 1 WHERE id 123; COMMIT;优点保证强一致性不会出现超卖缺点性能差锁竞争严重可能导致死锁不适用于高并发场景实战经验我们曾在QPS 500的场景使用悲观锁数据库连接很快被耗尽系统直接崩溃。2.3 Redis原子操作方案利用Redis的原子性操作local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then redis.call(DECR, KEYS[1]) return 1 else return 0 end优点性能极高Redis单线程模型实现相对简单缺点需要维护Redis和DB的数据一致性需要考虑Redis宕机情况需要处理库存回滚问题2.4 分布式锁方案使用Redis或Zookeeper实现分布式锁// 伪代码 try { boolean locked redisLock.lock(product_123, 3, TimeUnit.SECONDS); if(locked) { // 扣减库存 } } finally { redisLock.unlock(product_123); }优点适用于分布式系统相对灵活缺点实现复杂锁超时时间难以确定存在锁失效风险2.5 消息队列方案将订单请求先放入队列异步处理用户 - 下单请求 - 消息队列 - 库存服务单线程消费优点削峰填谷保护后端系统保证顺序处理缺点实时性差系统架构复杂需要处理消息堆积问题3. 电商系统实战解决方案3.1 分层防御架构设计经过多次大促的教训我们最终采用了分层防御的方案前端层本地计数限制防止用户疯狂点击随机延迟提交分散请求峰值活动预热提前加载页面资源接入层限流Nginx限流模块排队滑动窗口算法验证码分散用户请求服务层Redis预扣减原子操作分布式锁Redisson库存分段将库存拆分为多个段数据层最终一致性异步同步到DB库存流水表便于对账定期库存校准防止长期不一致3.2 RedisLua脚本实现这是我们线上使用的核心方案-- KEYS[1]: 商品库存key -- KEYS[2]: 商品售罄标记key -- ARGV[1]: 购买数量 local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then -- 已经售罄 return 0 elseif stock tonumber(ARGV[1]) then -- 扣减库存 redis.call(DECRBY, KEYS[1], ARGV[1]) -- 检查是否刚好售罄 local newStock tonumber(redis.call(GET, KEYS[1])) if newStock 0 then redis.call(SET, KEYS[2], 1, EX, 86400) end return 1 else -- 库存不足 return 0 end关键设计点使用DECRBY保证原子性售罄标记避免无效查询设置过期时间防止内存泄漏3.3 库存分段技术对于特别热门的商品如iPhone首发我们采用库存分段将1000件库存分为10个段每段100件每个段有独立的Redis key用户请求随机路由到某个段段内使用上述Lua脚本控制优势分散热点key压力提高并发处理能力降低单个key的竞争3.4 异步库存同步方案Redis与MySQL的库存同步设计1. 下单成功 - 写入订单表状态待支付 2. - 发送MQ消息库存扣减 3. 库存服务消费消息 - 真实扣减DB库存 4. 支付成功 - 更新订单状态 5. 支付超时 - 反向补回库存异常处理机制定时任务核对Redis和DB库存差异人工干预接口处理异常情况库存操作流水表记录所有变更4. 踩坑经验与优化建议4.1 必须避免的典型错误先查后改// 错误示范 int stock getStockFromDB(); if(stock 0) { updateStock(stock - 1); }这在并发下必然超卖过度依赖缓存忘记设置缓存过期时间缓存穿透导致DB压力缓存和DB不一致忽略网络延迟分布式锁的时钟漂移锁超时时间设置不合理没有考虑GC停顿影响4.2 性能优化技巧库存预热活动前将库存加载到Redis使用Pipeline批量初始化适当设置过期时间避免长期不一致本地缓存// 使用Guava Cache LoadingCacheString, Integer localCache CacheBuilder.newBuilder() .expireAfterWrite(100, TimeUnit.MILLISECONDS) .build(...);减少Redis访问压力热点Key处理添加随机后缀分散热点使用Redis Cluster分片考虑使用内存版Redis如阿里云Tair4.3 监控与告警必须建立的监控指标库存健康度Redis与DB库存差异库存变更流水异常负库存出现次数系统压力Redis QPS/CPU使用率DB连接池使用率消息队列堆积量业务指标下单失败率订单取消率库存售罄时间建议告警阈值Redis与DB库存差异5% → 警告下单失败率0.1% → 紧急告警负库存出现 → 立即电话告警5. 不同场景下的方案选型5.1 普通商品销售推荐方案Redis原子操作 异步同步实现要点读操作直接走Redis写操作使用Lua脚本异步消息同步到DB定时任务核对数据5.2 秒杀场景推荐方案库存分段 本地缓存 队列削峰额外措施前置验证用户资格检查令牌桶限流页面静态化订单过期自动释放5.3 预售模式特殊考虑虚拟库存概念超卖可能是业务需求需要明确预售规则特别注意超时未支付处理5.4 多仓库库存复杂点需要维护总库存和分仓库存要考虑仓库之间的调拨就近发货的库存分配需要更复杂的事务控制解决方案使用分布式事务如Seata采用最终一致性模式设计合理的超时和重试机制在实际项目中我们通常会根据业务特点混合使用多种方案。比如在618大促时对普通商品使用Redis原子操作对秒杀商品采用库存分段队列的方案同时配合完善的前端限流和监控告警系统。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑