设计一个支持10万QPS的秒杀系统需要从流量控制、系统架构、数据存储、并发安全等多个维度进行全方位优化,核心目标是削峰填谷、减少资源竞争、保证数据一致性。以下是具体设计方案:
一、系统架构总览
秒杀系统的核心挑战是瞬时高并发(10万QPS意味着每秒有10万次请求),需采用“分层过滤”思想,从前端到后端逐步减少无效请求,最终只让少量有效请求到达数据库。
整体架构分为6层:
用户层 → 前端层 → 接入层 → 应用层 → 缓存层 → 存储层
二、各层设计细节
1. 用户层:减少无效请求
-
前端限流:
- 按钮置灰:秒杀开始前按钮不可点击,防止提前请求;秒杀结束后立即置灰。
- 点击频率限制:通过JS限制用户每秒点击次数(如1次/秒),减少重复请求。
- 验证码/滑块:秒杀前要求用户完成验证,过滤机器人请求(可区分高峰期启用)。
-
预登录校验:
- 仅允许登录用户参与,减少匿名请求;通过Token验证用户合法性。
2. 前端层:静态资源优化
-
CDN加速:
- 秒杀页面的静态资源(HTML、CSS、JS、图片)全部通过CDN分发,避免回源请求。
- 页面预渲染:提前生成秒杀页面,减少动态渲染开销。
-
本地缓存:
- 浏览器缓存静态资源(设置合理的Cache-Control),减少重复加载。
3. 接入层:流量过滤与分发
-
负载均衡:
- 用Nginx作为入口,通过
upstream
将请求分发到多台应用服务器,避免单点压力。 - 配置
weight
权重,让性能更好的服务器承担更多流量。
- 用Nginx作为入口,通过
-
接入层限流:
- 基于Nginx的
limit_req
模块,对单个IP设置限流(如10次/秒),直接拦截高频恶意请求。 - 配置
burst
参数允许短暂突发流量,超过部分放入队列等待,避免直接拒绝。
# Nginx限流配置示例 http {limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;server {location /seckill {limit_req zone=seckill burst=20 nodelay; # 允许20个突发请求proxy_pass http://seckill_app_servers;}} }
- 基于Nginx的
-
协议优化:
- 采用HTTP/2或WebSocket,减少连接开销;非核心接口降级为HTTP/1.1。
4. 应用层:削峰填谷与业务校验
-
集群部署:
- 应用服务器水平扩容(如部署100台),通过K8s管理,支持动态扩缩容。
- 无状态设计:应用层不存储数据,便于水平扩展。
-
消息队列削峰:
- 秒杀请求不直接处理,而是先发送到消息队列(如RabbitMQ、Kafka),应用层异步消费。
- 队列长度设置阈值,超过则直接返回“秒杀拥挤”,避免队列堆积导致OOM。
// 伪代码:请求入队 @PostMapping("/seckill") public Result seckill(Long goodsId, String userId) {// 简单校验if (!checkUser(userId) || !checkGoods(goodsId)) {return Result.fail("无效请求");}// 发送到消息队列boolean success = mqTemplate.send("seckill_topic", new SeckillMsg(userId, goodsId));return success ? Result.ok("排队中") : Result.fail("系统繁忙"); }
-
业务校验:
- 重复购买校验:通过Redis记录用户已购商品(
user:seckill:{userId}
→ 商品ID集合),避免重复下单。 - 库存预热校验:若Redis中库存为0,直接返回“已抢完”,无需进入后续流程。
- 重复购买校验:通过Redis记录用户已购商品(
5. 缓存层:核心库存控制
-
Redis集群:
- 主从+哨兵架构(如3主3从),保证高可用;分片集群(如16个分片),分担存储压力。
- 库存存储:用Redis的
String
类型存储商品库存(seckill:stock:{goodsId}
→ 库存数量)。
-
原子操作控库存:
- 用Redis的
DECR
命令原子性减少库存,避免并发下超卖:// 伪代码:扣减库存 Long remain = redisTemplate.opsForValue().decrement("seckill:stock:" + goodsId); if (remain != null && remain >= 0) {// 库存充足,继续下单createOrder(userId, goodsId); } else {// 库存不足,回滚(INCR恢复)redisTemplate.opsForValue().increment("seckill:stock:" + goodsId);return "已抢完"; }
- 用Redis的
-
缓存预热:
- 秒杀开始前10分钟,将商品库存从数据库加载到Redis(
LOAD seckill:stock:{goodsId} FROM DB
)。 - 预热时设置Redis过期时间(如2小时),避免缓存长期无效占用空间。
- 秒杀开始前10分钟,将商品库存从数据库加载到Redis(
-
防缓存穿透:
- 对不存在的商品ID,在Redis中缓存
null
(短期过期),避免请求穿透到数据库。
- 对不存在的商品ID,在Redis中缓存
6. 存储层:最终一致性保证
-
数据库优化:
- 秒杀业务专用库:与主业务库分离,避免影响其他业务。
- 分库分表:按商品ID哈希分表(如16张表),减少单表压力。
- 索引优化:订单表建立
(user_id, goods_id)
联合索引,加速重复购买校验。
-
库存最终一致性:
- 异步更新:消息队列消费成功后,通过本地事务表+定时任务,确保订单信息最终写入数据库。
- 库存对账:秒杀结束后,对比Redis与数据库库存,若不一致(如Redis扣减成功但DB写入失败),通过补偿任务修正。
-
SQL优化:
- 扣减数据库库存时用
WHERE
条件保证原子性,避免超卖:UPDATE seckill_stock SET stock = stock - 1 WHERE goods_id = ? AND stock > 0;
- 若影响行数为1,则扣减成功;否则失败(此时需回滚Redis库存)。
- 扣减数据库库存时用
三、关键问题解决方案
-
超卖问题:
- Redis原子操作(
DECR
)保证缓存层库存准确。 - 数据库
WHERE stock > 0
的更新条件,作为最终保障。
- Redis原子操作(
-
库存不一致:
- 采用“缓存扣减优先,DB最终一致”策略,通过定时任务对账补偿。
- 消息队列确保订单创建与库存扣减的原子性(失败则重试)。
-
系统过载:
- 全链路限流:接入层、应用层、缓存层分别设置限流阈值。
- 降级策略:非核心接口(如商品详情)在流量高峰时降级为静态页面。
-
高可用:
- 多机房部署:跨地域容灾,避免单点机房故障。
- 熔断机制:用Sentinel/Hystrix监控接口响应时间,超时则熔断,返回友好提示。
四、性能压测与调优
- 压测工具:用JMeter模拟10万QPS流量,重点测试Redis响应时间、消息队列吞吐量、数据库写入性能。
- 瓶颈优化:
- 若Redis成为瓶颈:增加分片数、优化序列化方式(如用Protobuf替代JSON)。
- 若消息队列阻塞:扩容分区数、调整消费者线程池大小。
- 若数据库写入慢:增加从库、开启binlog并行复制。
总结
支持10万QPS的秒杀系统核心是“流量分层过滤+异步化+缓存优先+最终一致性”:
- 从前端到接入层过滤80%无效请求;
- 应用层通过消息队列削峰,将瞬时流量转为平稳流量;
- 缓存层用Redis原子操作控制库存,减少数据库访问;
- 存储层保证最终一致性,通过对账机制修正异常。