设计一个支持10万QPS的秒杀系统需要从流量控制、系统架构、数据存储、并发安全等多个维度进行全方位优化,核心目标是削峰填谷、减少资源竞争、保证数据一致性。以下是具体设计方案:

一、系统架构总览

秒杀系统的核心挑战是瞬时高并发(10万QPS意味着每秒有10万次请求),需采用“分层过滤”思想,从前端到后端逐步减少无效请求,最终只让少量有效请求到达数据库。

整体架构分为6层:

用户层 → 前端层 → 接入层 → 应用层 → 缓存层 → 存储层

二、各层设计细节

1. 用户层:减少无效请求

  • 前端限流

    • 按钮置灰:秒杀开始前按钮不可点击,防止提前请求;秒杀结束后立即置灰。
    • 点击频率限制:通过JS限制用户每秒点击次数(如1次/秒),减少重复请求。
    • 验证码/滑块:秒杀前要求用户完成验证,过滤机器人请求(可区分高峰期启用)。
  • 预登录校验

    • 仅允许登录用户参与,减少匿名请求;通过Token验证用户合法性。

2. 前端层:静态资源优化

  • CDN加速

    • 秒杀页面的静态资源(HTML、CSS、JS、图片)全部通过CDN分发,避免回源请求。
    • 页面预渲染:提前生成秒杀页面,减少动态渲染开销。
  • 本地缓存

    • 浏览器缓存静态资源(设置合理的Cache-Control),减少重复加载。

3. 接入层:流量过滤与分发

  • 负载均衡

    • 用Nginx作为入口,通过upstream将请求分发到多台应用服务器,避免单点压力。
    • 配置weight权重,让性能更好的服务器承担更多流量。
  • 接入层限流

    • 基于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;}}
    }
    
  • 协议优化

    • 采用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,直接返回“已抢完”,无需进入后续流程。

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 "已抢完";
      }
      
  • 缓存预热

    • 秒杀开始前10分钟,将商品库存从数据库加载到Redis(LOAD seckill:stock:{goodsId} FROM DB)。
    • 预热时设置Redis过期时间(如2小时),避免缓存长期无效占用空间。
  • 防缓存穿透

    • 对不存在的商品ID,在Redis中缓存null(短期过期),避免请求穿透到数据库。

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库存)。

三、关键问题解决方案

  1. 超卖问题

    • Redis原子操作(DECR)保证缓存层库存准确。
    • 数据库WHERE stock > 0的更新条件,作为最终保障。
  2. 库存不一致

    • 采用“缓存扣减优先,DB最终一致”策略,通过定时任务对账补偿。
    • 消息队列确保订单创建与库存扣减的原子性(失败则重试)。
  3. 系统过载

    • 全链路限流:接入层、应用层、缓存层分别设置限流阈值。
    • 降级策略:非核心接口(如商品详情)在流量高峰时降级为静态页面。
  4. 高可用

    • 多机房部署:跨地域容灾,避免单点机房故障。
    • 熔断机制:用Sentinel/Hystrix监控接口响应时间,超时则熔断,返回友好提示。

四、性能压测与调优

  • 压测工具:用JMeter模拟10万QPS流量,重点测试Redis响应时间、消息队列吞吐量、数据库写入性能。
  • 瓶颈优化
    • 若Redis成为瓶颈:增加分片数、优化序列化方式(如用Protobuf替代JSON)。
    • 若消息队列阻塞:扩容分区数、调整消费者线程池大小。
    • 若数据库写入慢:增加从库、开启binlog并行复制。

总结

支持10万QPS的秒杀系统核心是“流量分层过滤+异步化+缓存优先+最终一致性”:

  • 从前端到接入层过滤80%无效请求;
  • 应用层通过消息队列削峰,将瞬时流量转为平稳流量;
  • 缓存层用Redis原子操作控制库存,减少数据库访问;
  • 存储层保证最终一致性,通过对账机制修正异常。