java秒杀设计

4 阅读7分钟

秒杀系统设计

秒杀系统是典型的高并发、高负载场景,核心挑战是在极短时间内处理大量用户请求,保证不超卖系统不崩溃用户体验可接受。下面从架构、流程、关键技术、数据一致性、高可用等方面展开设计。


一、需求与挑战

业务目标

  • 例如:限量商品(如 1000 部手机)在指定时间开抢,预期数十万甚至上百万用户同时参与。
  • 系统需要在秒级内完成:校验资格 → 预扣库存 → 生成订单 → 异步处理后续(支付等)。

核心挑战

  • 瞬时高并发:QPS 可能从几十瞬间飙升至数万甚至数十万。
  • 热点商品:库存单一,数据库热点行成为瓶颈。
  • 超卖:并发扣减库存时,必须保证最终库存不为负数。
  • 恶意刷单:自动化脚本、黄牛等攻击手段。
  • 系统稳定性:防止缓存击穿、数据库雪崩、服务熔断。

二、总体架构

text

┌─────────┐     ┌─────────┐     ┌───────────────────┐
│  前端   │────▶│   CDN   │────▶│   负载均衡 (Nginx) │
└─────────┘     └─────────┘     └───────────────────┘
                                        │
                        ┌───────────────┼───────────────┐
                        ▼               ▼               ▼
                ┌───────────────┐ ┌───────────┐ ┌───────────┐
                │  Web 应用集群 │ │ 限流组件  │ │ 风控服务  │
                └───────────────┘ └───────────┘ └───────────┘
                        │               │               │
                        └───────────────┼───────────────┘
                                        ▼
                            ┌───────────────────────┐
                            │   Redis 集群(缓存)   │
                            │ - 库存预热            │
                            │ - 限流计数器          │
                            │ - 用户资格/黑名单     │
                            └───────────────────────┘
                                        │
                                        ▼
                            ┌───────────────────────┐
                            │   消息队列 (RocketMQ)  │
                            │ - 削峰填谷            │
                            │ - 异步下单            │
                            └───────────────────────┘
                                        │
                                        ▼
                            ┌───────────────────────┐
                            │     数据库主从         │
                            │ - 订单表(分库分表)   │
                            │ - 库存变更记录        │
                            └───────────────────────┘

三、核心流程

1. 秒杀前(准备阶段)

  • 库存预热:将商品库存从数据库加载到 Redis(如 SET seckill:stock:{id} 1000)。
  • 页面静态化:商品详情页、秒杀页全静态,通过 CDN 加速,减少后端压力。
  • 限流规则配置:如单用户访问频率、总请求阈值等。
  • 预置令牌/资格:可提前通过活动资格校验(如积分、会员等级)生成 token 存入 Redis,避免开抢时大量 DB 查询。

2. 用户请求到达

  • 前端限流:按钮置灰、验证码(图形或滑块)防止机器刷单。

  • CDN/负载均衡:分发静态资源,动态请求转发至应用集群。

  • 统一入口网关(Nginx/Spring Cloud Gateway):

    • 进行全局限流(如基于 IP 的令牌桶)。
    • 校验用户登录态,并将请求路由到后端。

3. 应用层处理

应用层无状态,水平扩展,核心逻辑如下:

java

// 伪代码
@PostMapping("/seckill")
public Result seckill(Long userId, Long productId) {
    // 1. 用户限流(Redis incr + 过期时间)
    if (!rateLimiter.tryAcquire(userId, 1, 10)) {
        return Result.error("请求太频繁");
    }
    // 2. 资格校验(是否已秒杀过、是否满足活动条件)
    if (hasOrder(userId, productId)) {
        return Result.error("已参与过");
    }
    // 3. Redis 预扣库存(Lua 脚本原子操作)
    Long stock = redisTemplate.execute(SCRIPT, 
        Collections.singletonList("seckill:stock:" + productId), 
        Collections.singletonList("1"));
    if (stock == null || stock < 0) {
        return Result.error("库存不足");
    }
    // 4. 发送消息到队列,异步创建订单
    sendToQueue(buildSeckillMessage(userId, productId));
    return Result.success("抢购中,请稍后查看订单");
}
库存扣减 Lua 脚本

lua

local key = KEYS[1]
local decr = tonumber(ARGV[1])
local stock = redis.call('get', key)
if not stock then
    return -1
end
stock = tonumber(stock)
if stock >= decr then
    redis.call('decrby', key, decr)
    return stock - decr
else
    return -1
end

返回剩余库存(>=0 成功,-1 失败)。

4. 异步下单(消息队列)

  • 消息体包含用户ID、商品ID、秒杀时间戳等。

  • 消费端处理:

    • 再次校验库存(最终以数据库为准,可使用乐观锁)。
    • 插入订单,更新数据库库存。
    • 如果数据库扣减失败(如并发超卖),则触发补偿:回滚 Redis 库存(通过消息重试或落库失败补偿)。
  • 消息消费失败可重试,超过次数后进入死信队列人工介入。

5. 客户端轮询结果

  • 前端定时请求查询订单状态(例如 3 秒一次,最多 10 次)。
  • 后台根据 Redis 或数据库返回“排队中/成功/失败”。

四、关键技术细节

1. 防止超卖

  • Redis 预扣库存:利用单线程原子性减少数据库压力,但不绝对保证最终一致性。
  • 数据库乐观锁:更新库存时使用 UPDATE ... SET stock = stock - 1 WHERE product_id = ? AND stock > 0,通过受影响行数判断是否成功。
  • 最终一致性:以数据库为准,若数据库扣减失败,需通过补偿机制恢复 Redis 库存(可发送补偿消息或记录失败日志)。

2. 限流与防刷

  • 令牌桶/漏桶:在网关或应用层使用 Guava RateLimiter 或 Sentinel 限制单 IP/单用户的 QPS。
  • 滑动窗口:使用 Redis 的 ZSet 记录用户请求时间戳,实现更精确的限流。
  • 验证码:开抢前要求输入图形验证码或拖动验证,增加机器成本。
  • 风控服务:识别异常 IP、设备指纹、用户行为,拦截黑产。

3. 缓存与热点

  • 库存数据全量在 Redis,避免读取数据库。
  • 使用 Redis Cluster 分散热点,避免单节点压力。
  • 本地缓存(如 Caffeine):将商品基础信息(价格、活动规则)缓存到应用内存,减少 Redis 访问。

4. 削峰填谷

  • 消息队列将瞬时请求异步化,拉平高峰,保护下游数据库。
  • 队列长度需监控,防止堆积过多导致消费者崩溃;可设置最大积压告警。

5. 服务降级与熔断

  • 熔断:若数据库或 Redis 响应缓慢,通过 Hystrix/Sentinel 快速失败,返回友好提示。
  • 降级:关闭非核心功能(如推荐、评论),优先保障秒杀主流程。

6. 数据库设计

  • 订单表按用户 ID 或时间分库分表(ShardingSphere)。

  • 库存表可设计为:

    sql

    CREATE TABLE seckill_stock (
        id BIGINT PRIMARY KEY,
        product_id BIGINT NOT NULL,
        stock INT NOT NULL,
        version INT DEFAULT 0, -- 乐观锁
        ...
    );
    

    更新时使用 update ... set stock = stock - 1, version = version + 1 where product_id = ? and stock > 0 and version = ?


五、高可用与扩展

部署架构

  • 多机房多活:流量分发至不同数据中心,单机房故障时快速切换。
  • 无状态应用:Web 应用水平扩展,通过 K8s 自动伸缩。
  • Redis Cluster:主从+哨兵,自动故障转移。
  • 消息队列集群:RocketMQ 主从同步,保证消息不丢失。
  • 数据库:读写分离,主库写,从库读;分库分表后每个库可独立主从。

监控与告警

  • 指标:QPS、响应时间、库存剩余、队列堆积、数据库连接数、Redis 命中率。
  • 链路追踪:SkyWalking 定位慢请求。
  • 告警:如库存 Redis 值异常、队列积压超过阈值、错误率飙升时,及时通知值班人员。

六、整体时序图(简化)

text

用户 ──▶ CDN ──▶ Nginx ──▶ 应用
                                 │
                                 ▼
                            Redis(库存扣减)
                                 │
                                 ▼
                              成功 → 发送消息 → RocketMQ → 消费者 → 数据库
                                 │                        │
用户 ◀─────────────────── 轮询结果 ◀───────────────────────┘

七、扩展思考

1. 是否可以用数据库直接扣减?

可以,但需要配合乐观锁+排队。但纯数据库处理数十万 QPS 很难,必须引入缓存层。

2. 万一 Redis 挂了怎么办?

  • 主从切换,但切换期间有短暂不可用。
  • 可结合本地缓存+降级方案,比如直接放行少量请求(减少损失),或关闭秒杀入口。

3. 如何应对流量突增?

  • 通过自动扩缩容(K8s HPA)增加实例数。
  • 提前压测,确定单机上限,设置合理的实例数量。
  • 启用限流,超出预估的请求直接返回“繁忙”。

4. 用户体验如何优化?

  • 秒杀按钮倒计时同步(服务器时间)。
  • 返回“排队中”而不是“失败”,减少用户刷新。
  • 提供结果通知(如短信、站内信)增强体验。

八、总结

秒杀系统的核心设计思路是:层层过滤,逐级削峰,最终一致性

  • 前端层:限流、静态化、CDN。
  • 接入层:负载均衡、网关限流、风控。
  • 应用层:无状态、本地缓存、服务降级。
  • 缓存层:Redis 原子操作、库存预热。
  • 异步层:消息队列削峰,异步落库。
  • 存储层:分库分表、乐观锁保证最终一致性。

在具体实现时,根据业务规模和资金投入,可适当简化或增强某些环节,但上述原则基本适用。