面试官灵魂拷问:如何设计一个支持 10 万 QPS 的秒杀系统?

八年经验谈:高并发秒杀系统的全链路设计与实现

作为一名经历过多次电商大促考验的 Java 后端开发者,我深知秒杀系统是对技术架构的终极考验。它不仅需要应对瞬时万级 QPS 的冲击,更要在库存安全、用户体验、成本控制之间找到平衡。本文将从业务痛点出发,分享一套经过实战验证的秒杀系统设计方案,涵盖架构分层、核心模块实现与工程化经验。

一、业务特性与核心挑战分析

1. 秒杀业务的三大核心特性

  • 流量突增:日常流量 100QPS → 秒杀瞬时 10 万 + QPS(典型 1000 倍突发)
  • 库存有限:单个商品库存通常≤1000 件,库存扣减必须精准(避免超卖 / 少卖)
  • 短事务性:核心流程仅包含 "库存校验→扣减→订单生成",要求 RT<50ms

2. 技术实现的五大痛点

痛点 传统方案问题 秒杀场景特殊需求
流量洪峰 数据库连接池打满 前端限流 + 多级缓存削峰
库存超卖 乐观锁失效(ABA 问题) 内存级原子操作 + 数据库强校验
热点商品竞争 分布式锁性能瓶颈 分片锁 + 无锁化设计
恶意请求 爬虫工具批量请求 人机校验 + 频率控制
流量不均 缓存雪崩 / 穿透 库存预热 + 热点隔离

二、全链路架构分层设计

1. 七层防护架构图

  • ① 前端层 → 接入层:按钮防重复点击(防用户重复提交)
  • ② 网关层 → 接入层:令牌桶限流(流量控制)
  • ③ 接入层 → 应用层:人机校验(防御自动化攻击)
  • ④ 应用层 → 缓存层:队列削峰(应对流量高峰)
  • ⑤ 缓存层 → 数据库层:库存预热(预加载热点数据)
  • ⑥ 数据库层 → 存储层:行锁优化(并发控制)
  • ⑦ 存储层 → 日志系统:异步落盘(提升IO性能)

2. 关键分层设计解析

(1)前端层:流量第一道防线
  • 按钮置灰:点击后禁用 3 秒,拦截 50% 重复请求
  • 动态令牌:调用秒杀接口前需先获取 Redis 令牌(seckill:token:{userId})
  • 浏览器缓存:缓存秒杀倒计时,减少无效 API 调用
(2)网关层:流量清洗中心
less 复制代码
// 基于Spring Cloud Gateway的限流配置
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("seckill", r -> r.path("/seckill/**")
            .filters(f -> f.requestRateLimiter(config -> 
                config.setRedisRateLimiter(
                    new RedisRateLimiter(10, 20) // 每秒10请求,突发容量20
                )
            ))
            .uri("lb://seckill-service"))
        .build();
}
(3)应用层:业务逻辑核心
  • 独立域名隔离 :秒杀业务使用独立域名(seckill.example.com),避免影响主站
  • 线程池隔离:为秒杀业务单独配置线程池(corePoolSize=200, maxPoolSize=500)
(4)缓存层:库存前置处理
  • 双写一致性:Redis 库存与 DB 库存通过异步队列保持最终一致
  • 热点分片:按商品 ID 哈希分片(如seckill:stock:1001),分散 Redis 压力

三、核心模块实现详解

1. 库存预热模块(核心代码)

typescript 复制代码
public class StockPreheatService {
    private final JedisCluster jedisCluster;
    private final SeckillGoodsMapper goodsMapper;
    // 预热库存到Redis(活动开始前10分钟执行)
    public void preheatStock(Long goodsId, Integer stock) {
        // 初始化库存(使用Lua脚本保证原子性)
        String luaScript = "if redis.call('exists', KEYS[1]) == 0 then " +
                           "redis.call('set', KEYS[1], ARGV[1]) " +
                           "redis.call('set', KEYS[2], ARGV[2]) end";
        jedisCluster.eval(luaScript, 2, 
            "seckill:stock:" + goodsId, // 库存键
            "seckill:version:" + goodsId, // 版本号键
            String.valueOf(stock), 
            "1"); // 初始版本号
    }
    // 扣减库存(无锁化设计)
    public boolean deductStock(Long goodsId) {
        String luaScript = "local stock = tonumber(redis.call('get', KEYS[1])) " +
                           "if stock > 0 then " +
                           "redis.call('decr', KEYS[1]) " +
                           "redis.call('incr', KEYS[2]) " +
                           "return 1 " +
                           "else " +
                           "return 0 " +
                           "end";
        Long result = (Long) jedisCluster.eval(luaScript, 2, 
            "seckill:stock:" + goodsId, 
            "seckill:version:" + goodsId);
        return result == 1;
    }
}

2. 分布式令牌生成(防刷机制)

vbnet 复制代码
public class TokenService {
    private final RedisTemplate<String, Integer> redisTemplate;
    // 生成秒杀令牌(每个用户限领1个)
    public boolean generateToken(Long userId, Long goodsId) {
        String key = "seckill:token:" + userId + ":" + goodsId;
        return redisTemplate.opsForValue().setIfAbsent(key, 1, 60, TimeUnit.SECONDS);
    }
    // 校验令牌并删除(防止重复使用)
    public boolean validateToken(Long userId, Long goodsId) {
        String key = "seckill:token:" + userId + ":" + goodsId;
        return redisTemplate.delete(key);
    }
}

3. 异步队列削峰(Kafka 实现)

typescript 复制代码
public class SeckillProducer {
    private final KafkaTemplate<String, SeckillRequest> kafkaTemplate;
    // 发送秒杀请求到队列(削峰填谷)
    public void sendSeckillRequest(SeckillRequest request) {
        kafkaTemplate.send("seckill_topic", request.getGoodsId().toString(), request);
    }
}
@Service
public class SeckillConsumer {
    private final StockPreheatService stockService;
    private final OrderService orderService;
    @KafkaListener(topics = "seckill_topic", groupId = "seckill_group")
    public void processSeckillRequest(SeckillRequest request) {
        // 1. 库存扣减
        if (stockService.deductStock(request.getGoodsId())) {
            // 2. 生成订单(数据库事务)
            createOrder(request);
        }
    }
    private void createOrder(SeckillRequest request) {
        OrderEntity order = new OrderEntity();
        order.setGoodsId(request.getGoodsId());
        order.setUserId(request.getUserId());
        order.setCreateTime(LocalDateTime.now());
        orderService.save(order);
    }
}

四、数据库层防超卖设计

1. 库存扣减的三级校验

  1. Redis 预扣:通过 Lua 脚本保证原子性扣减(内存级校验)
  1. 数据库行锁:扣减时使用SELECT ... FOR UPDATE锁定库存记录
  1. 版本号校验:通过UPDATE goods SET stock = stock - 1 WHERE id=? AND version=?防止 ABA 问题

2. 数据库表结构优化

sql 复制代码
CREATE TABLE seckill_goods (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    goods_id BIGINT NOT NULL COMMENT '商品ID',
    stock INT NOT NULL COMMENT '库存',
    version INT DEFAULT 0 COMMENT '乐观锁版本号',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 扣减库存SQL(带版本号校验)
UPDATE seckill_goods 
SET stock = stock - 1, version = version + 1 
WHERE goods_id = ? AND stock > 0 AND version = ?

五、工程化最佳实践

1. 压测与容量规划

  • 基准测试:使用 JMeter 模拟 1 万并发,单节点压测阈值 QPS=800
  • 弹性扩展:根据压测结果,按 1:1000 比例部署服务器(10 万 QPS 需 125 台节点)
  • 应急预案:准备熔断组件(Hystrix),当 Redis 响应时间 > 100ms 时熔断库存查询

2. 监控与报警体系

  • 核心指标
    • Redis 命中率(目标 > 99%)
    • 队列堆积量(阈值 10 万条)
    • 数据库连接数(阈值 80%)
  • 报警机制:通过 Prometheus+Grafana 实时监控,异常时触发企业微信 / 短信报警

3. 流量调度策略

  • 预热期(活动前 30 分钟) :逐步增加 CDN 节点缓存,同步预热 Redis 集群
  • 高峰期(活动开始后 5 分钟) :启用 Nginx 限流模块(limit_req_zone),拒绝超过阈值的请求
  • 降温期(库存售罄后) :返回友好提示页面,关闭异步队列消费线程

六、避坑指南与经验总结

1. 三大核心坑点解决方案

问题场景 传统方案缺陷 秒杀系统解决方案
热点商品 Redis 分片不均 单节点压力过大 按商品 ID 哈希分片(如取模 1024)
队列消费失败导致漏单 重试机制不完善 引入死信队列 + 人工补偿接口
浏览器缓存导致库存显示不一致 缓存更新不及时 采用 Stale-While-Revalidate 策略

2. 八年实战经验总结

  1. 能在内存解决的问题,绝不下数据库:90% 的性能问题可以通过 Redis 预热解决
  1. 分布式锁不是银弹:优先使用无锁化设计(如 AtomicLong/Lua 脚本),必须加锁时采用分片锁
  1. 流量控制比流量处理更重要:前端 + 网关层至少过滤 80% 的无效请求
  1. 最终一致性优于强一致性:订单生成可异步处理,库存扣减必须保证强一致
相关推荐
编程乐学(Arfan开发工程师)4 小时前
56、原生组件注入-原生注解与Spring方式注入
java·前端·后端·spring·tensorflow·bug·lua
周某某~5 小时前
七.适配器模式
java·设计模式·适配器模式
Elcker6 小时前
Springboot+idea热更新
spring boot·后端·intellij-idea
奔跑的小十一7 小时前
JDBC接口开发指南
java·数据库
刘大猫.7 小时前
业务:资产管理功能
java·资产管理·资产·资产统计·fau·bpb·mcb
GISer_Jing7 小时前
JWT授权token前端存储策略
前端·javascript·面试
YuTaoShao7 小时前
Java八股文——JVM「内存模型篇」
java·开发语言·jvm
开开心心就好7 小时前
电脑扩展屏幕工具
java·开发语言·前端·电脑·php·excel·batch
拉不动的猪7 小时前
es6常见数组、对象中的整合与拆解
前端·javascript·面试
蒟蒻小袁7 小时前
力扣面试150题--单词接龙
算法·leetcode·面试