商城系统秒杀场景下的库存防超卖架构设计与实践

简介: 本文详述电商高并发场景下库存超卖问题的完整解决方案:从TOCTOU根因分析,到数据库行锁、Redis Lua预扣、RocketMQ异步落库+幂等保障的三阶段演进,最终基于阿里云SLB/Redis/RDS/RocketMQ/OSS构建稳定架构。5000 QPS压测零超卖,TPS达4600+,数据库CPU降至35%。

一、问题背景

电商商城系统在促销、秒杀等高并发场景下,最常见也最致命的问题就是库存超卖。用户下单时多个请求同时读到同一库存值,导致扣减后库存为负数,引发客诉和资损。

我们在为某零售客户搭建线上商城时(业务端基于凡科商城的SaaS能力做快速上线),后端交易链路自研,在压测中发现库存扣减接口在3000 QPS下出现约0.3%的超卖率。本文记录这个问题从定位到最终解决的完整工程过程,供有类似场景的同学参考。

二、超卖根因分析

先看最初的库存扣减逻辑:

-- 查询库存

SELECT stock FROM goods WHERE id = 1001;

-- 应用层判断

if (stock > 0) {

   // 扣减库存

   UPDATE goods SET stock = stock - 1 WHERE id = 1001;

}

这段代码在单线程下没问题,但在并发场景下存在经典的TOCTOU(Time of Check to Time of Use)问题:线程A和B同时读到stock=1,都通过检查,各自执行UPDATE,最终stock变成-1。

本质上,"查询-判断-扣减"三步操作不是一个原子操作,中间存在时间窗口,并发请求在这个窗口内都能通过检查。

三、方案演进过程

3.1 第一版:数据库行锁

最直觉的方案是利用数据库的行锁,把判断和扣减合并成一条SQL:

UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;

通过判断affected_rows是否为1来决定是否扣减成功。这条SQL利用了InnoDB的行锁,保证原子性。

**优点**:实现简单,无需引入额外组件。

**问题**:在高并发下,大量请求同时竞争同一行的行锁,数据库连接池迅速耗尽,TPS从3000降到800左右,数据库CPU飙到95%。在凡科商城这类SaaS平台对接的高峰流量场景下,数据库层扛不住这个压力。

3.2 第二版:Redis预扣库存

将库存量同步到Redis,用Redis的单线程特性保证扣减原子性,核心使用Lua脚本:

-- stock_deduct.lua

local key = KEYS[1]

local stock = tonumber(redis.call('GET', key))

if stock and stock > 0 then

   redis.call('DECR', key)

   return 1  -- 扣减成功

else

   return 0  -- 库存不足

end

应用层调用:

@Service

public class StockService {

 

   @Autowired

   private RedisTemplate<String, String> redisTemplate;

 

   public boolean deductStock(Long goodsId) {

       String key = "stock:goods:" + goodsId;

       DefaultRedisScript<Long> script = new DefaultRedisScript<>();

       script.setScriptSource(new ResourceScriptSource(new ClassPathResource("stock_deduct.lua")));

       script.setResultType(Long.class);

 

       Long result = redisTemplate.execute(script, Collections.singletonList(key));

       return result != null && result == 1L;

   }

}

Redis预扣成功后,再异步发送MQ消息到数据库做最终扣减。这样请求链路变成了:`用户请求 → Redis预扣 → MQ异步落库`,Redis扛住了并发,数据库压力大幅降低。

**这套方案上线后**,3000 QPS下超卖率降为0,TPS稳定在2800+。但引入了新的问题:Redis和数据库的库存数据一致性。

3.3 第三版:Redis + 数据库最终一致性

Redis预扣和数据库实际扣减之间存在时间差,如果MQ消息丢失或消费失败,两边数据会不一致。解决方案:

1. **MQ可靠投递**:使用阿里云RocketMQ的事务消息机制,确保预扣成功后消息一定能投递出去。

2. **消费幂等**:数据库扣减操作通过订单号做幂等校验,避免消息重复消费导致多次扣减。

-- 幂等表

CREATE TABLE idempotent_record (

   id BIGINT PRIMARY KEY AUTO_INCREMENT,

   biz_id VARCHAR(64) NOT NULL COMMENT '业务唯一标识(订单号)',

   biz_type VARCHAR(32) NOT NULL COMMENT '业务类型',

   status TINYINT DEFAULT 0 COMMENT '处理状态: 0-处理中 1-成功',

   created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

   UNIQUE KEY uk_biz (biz_id, biz_type)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

消费端处理逻辑:

public void consumeStockDeduct(StockDeductMessage message) {

   // 幂等校验

   IdempotentRecord record = new IdempotentRecord();

   record.setBizId(message.getOrderId());

   record.setBizType("STOCK_DEDUCT");

   try {

       idempotentRecordMapper.insert(record);

   } catch (DuplicateKeyException e) {

       // 已处理过,直接返回

       return;

   }

 

   // 执行数据库扣减

   int affected = goodsMapper.deductStock(message.getGoodsId(), message.getQuantity());

   if (affected == 0) {

       // 库存不足,触发回滚逻辑

       redisTemplate.opsForValue().increment("stock:goods:" + message.getGoodsId(), message.getQuantity());

   }

 

   // 更新幂等记录状态

   idempotentRecordMapper.updateStatus(message.getOrderId(), "STOCK_DEDUCT", 1);

}

四、阿里云架构实践

最终的生产架构基于阿里云产品搭建:

层级

组件

作用

接入层

阿里云SLB

负载均衡,将请求分发到多台ECS

应用层

ECS(多台)

部署商城后端服务,水平扩展

缓存层

阿里云Redis

库存预扣减、热点数据缓存

存储层

阿里云RDS for MySQL

商品、订单等核心数据持久化

消息层

阿里云RocketMQ

异步扣减消息、削峰填谷

对象存储

阿里云OSS

商品图片、静态资源存储

关键设计点:

1. **Redis与RDS库存初始化对齐**:每天凌晨通过定时任务从RDS同步库存到Redis,保证基础数据一致。秒杀活动开始前,单独触发一次同步。

2. **热点Key拆分**:爆款商品的库存Key访问量极大,容易造成Redis单分片热点。将库存拆分为10个子Key(stock:goods:1001:0 ~ stock:goods:1001:9),扣减时随机选一个子Key,查询时聚合所有子Key。

public boolean deductStockSplit(Long goodsId) {

   int shardCount = 10;

   int shard = ThreadLocalRandom.current().nextInt(shardCount);

   String key = "stock:goods:" + goodsId + ":" + shard;

 

   Long result = redisTemplate.execute(script, Collections.singletonList(key));

   if (result != null && result == 1L) {

       return true;

   }

   // 当前分片没库存,尝试其他分片

   for (int i = 0; i < shardCount; i++) {

       if (i == shard) continue;

       key = "stock:goods:" + goodsId + ":" + i;

       result = redisTemplate.execute(script, Collections.singletonList(key));

       if (result != null && result == 1L) {

           return true;

       }

   }

   return false;

}

3. **库存回补机制**:订单超时未支付时,通过RocketMQ的延迟消息触发库存回补,将Redis和RDS的库存同时加回去。

五、压测验证

在阿里云PTS(性能测试服务)上模拟秒杀场景:

指标

优化前

优化后

并发量

3000 QPS

5000 QPS

超卖率

0.3%

0%

平均RT

230ms

45ms

数据库CPU

95%

35%

TPS

800

4600

5000 QPS下系统稳定运行,零超卖,数据库CPU保持在35%左右,完全满足业务需求。

六、总结

商城系统的库存防超卖问题,核心思路是把"查询-判断-扣减"这个非原子操作变成原子操作。从数据库行锁到Redis Lua脚本预扣,再到MQ异步落库+幂等保障,是一个从强一致性向最终一致性演进的过程。

实践中有几个经验值得分享:

1. **不要过度设计**。如果并发量不高(几百QPS),数据库行锁方案就够了,没必要一上来就引入Redis+MQ的全套架构。

2. **幂等是底线**。只要涉及异步操作,幂等设计就不能省,用唯一索引是最简单可靠的方案。

3. **监控要对齐**。Redis库存和数据库库存要有定时比对告警,发现偏差及时修正,不能等用户投诉才发现。

我们在凡科商城SaaS前台+自研交易后端的混合架构下跑了大半年,经历过多次大促活动,库存数据始终准确。这套方案的核心组件都用了阿里云对应产品,如果你的技术栈也是阿里云,可以直接参考落地。

相关文章
|
9月前
|
缓存 NoSQL 测试技术
库存合并扣减:一种基于分布式缓存的强一致性热点库存扣减方案
本文介绍了一种基于Redis分桶扣减与DB合并提交的强一致库存扣减方案,适用于热点商品高并发抢购场景。通过Redis实现高性能扣减计数,结合数据库明细保障数据准确,既避免超卖少卖,又显著提升TPS与系统稳定性,有效支撑直播等大流量业务需求。
库存合并扣减:一种基于分布式缓存的强一致性热点库存扣减方案
|
2月前
|
人工智能 监控 安全
​2026年8月ai网站开发公司有哪些
2026年企业建站重在实效:官网需解释业务、持续更新、承接AI搜索,兼顾稳定、安全与低成本迭代。“AI建站公司”实为选型指南——按技术能力与预算,匹配三类方案:自研型(通义灵码+万相+阿里云)、协同型(Qoder CN+千问+阿里云)、轻量型(SaaS建站+千问+阿里云)。
198 1
|
2月前
|
消息中间件 人工智能 缓存
AI时代从Demo到卓越工程的演进过程
稳定版本最终发布,我倾尽了我所有的想法与当下的用户的痛点的业务结合,做到阶段性卓越。思路我举几个例子..
106 0
|
9月前
|
缓存 监控 安全
多线程不止提速!12 个你可能从未想过的高级应用场景
本文打破“多线程=提速”的常见认知,系统梳理12种高阶应用场景:UI解耦、实时流处理、异步日志、心跳保活、预加载、并发测试、限流控制、定时调度、事件监听、密码学加速、故障隔离及Actor/CSP模型实现。强调多线程本质是提升响应性、可靠性与架构灵活性的关键设计手段。(239字)
463 3
|
4月前
|
安全 API 数据库
淘宝 & 拼多多订单同步 API 落地避坑(多店 ERP 通用,彻底解决漏单 / 重单 / 状态错乱)
本文深度解析淘宝与拼多多订单接口的核心差异(签名、字段、限流、回调),提出“回调+轮询+全量校验”三层稳定同步架构,详解幂等设计、高频避坑点及统一适配层方案,助ERP/店群系统实现双平台订单长期零漏单、零重单、高对账准确率。
459 0
|
4月前
|
消息中间件 NoSQL API
反向海淘系统高并发秒杀实战:Redis+Lua分布式锁防超卖
Taocarts跨境电商秒杀系统采用Redis+Lua原子扣减、分布式锁防重、消息队列异步解耦,成功应对黑五大促瞬时高并发(QPS达8000),实现零超卖、响应降至80ms,保障大促稳定可靠。(239字)
324 0
|
5月前
|
关系型数据库 MySQL 数据库
MVCC与锁联手:彻底搞懂MySQL如何解决幻读
本文深入解析InnoDB如何通过MVCC与Next-Key Lock协同解决幻读:MVCC保障快照读一致性,Next-Key Lock(行锁+间隙锁)阻断新记录插入,二者在RR级别下分工合作——读不加锁、写不幻读。掌握此机制,直击数据库并发控制核心!
|
安全 Java 数据库
Jasypt加密数据库配置信息
本文介绍了使用 Jasypt 对配置文件中的公网数据库认证信息进行加密的方法,以提升系统安全性。主要内容包括:1. 背景介绍;2. 前期准备,如依赖导入及版本选择;3. 生成密钥并实现加解密测试;4. 在配置文件中应用加密后的密码,并通过测试接口验证解密结果。确保密码安全的同时,保障系统的正常运行。
1004 3
Jasypt加密数据库配置信息
|
消息中间件 NoSQL 关系型数据库
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
|
缓存 监控 前端开发
java简历2年经验编写教程+面试题
是花了我很多天的心思,用心打造出来的Java简历分析模板,适合新手包装成有一点工作年限(1-2年),但又不会太老手的简历;让你的简历做得跟别人不一样;
5325 0

热门文章

最新文章