Skip to content
第 233 / 250 章架构⏱ 18 分钟阅读

第 233 章:秒杀系统设计

学习目标

  • 秒杀业务核心挑战
  • 系统分层设计
  • 防超卖方案
  • 高并发实战

一、秒杀特点

特点描述
瞬时高并发1 秒内 100 万请求
读多写少查询 >> 下单
少量数据几个 SKU 抢
公平性防脚本、防羊毛党
超卖为 0严格不超卖

二、秒杀架构演进

2.1 第一版:前端拦截

90% 请求在前端拦截。

2.2 第二版:Nginx + Redis

独立部署秒杀服务,与正常业务隔离。

2.3 第三版:消息队列削峰

三、防超卖设计

3.1 Redis 原子操作

java
public class SeckillService {

    private static final String STOCK_KEY = "seckill:stock:";
    private static final String ORDER_KEY = "seckill:order:";

    private static final String SEC_LUA = """
        local stock = tonumber(redis.call('GET', KEYS[1]))
        if stock and stock > 0 then
            redis.call('DECR', KEYS[1])
            return 1
        end
        return 0
        """;

    public Result<String> seckill(String skuId, String userId) {
        // 1. 限流 + 验证
        if (!fraudCheck(userId)) {
            return Result.error("RISK_USER", "风险用户");
        }

        // 2. Redis 原子扣减
        Long ok = redisTemplate.execute(
            RedisScript.of(SEC_LUA, Long.class),
            List.of(STOCK_KEY + skuId),
            skuId
        );

        if (ok != 1) {
            return Result.error("SOLD_OUT", "已售罄");
        }

        // 3. 投递 MQ,异步落单
        messageTemplate.convertAndSend("seckill-orders",
            new SeckillMessage(skuId, userId, Instant.now()));

        return Result.ok("抢购成功,正在处理");
    }
}

3.2 库存签名校验

java
public String signSku(Long skuId) {
    String sign = DigestUtils.md5Hex(skuId + ":" + SALT);
    return sign.substring(0, 8);
}

// 前端:URL 加 ?token=签名
// 后端:校验签名,确保不是伪造请求

3.3 限购

java
private static final String LIMIT_LUA = """
    if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then
        return 0    -- 已购买过
    end
    redis.call('SADD', KEYS[1], ARGV[1])
    return 1
    """;

public boolean tryAcquire(Long skuId, String userId) {
    Long ok = redisTemplate.execute(
        RedisScript.of(LIMIT_LUA, Long.class),
        List.of("seckill:bought:" + skuId),
        userId
    );
    return ok == 1;
}

四、MQ 削峰

异步消费端:

java
@RocketMQMessageListener(topic = "seckill-orders")
public class SeckillConsumer implements RocketMQListener<SeckillMessage> {

    @Override
    public void onMessage(SeckillMessage msg) {
        try {
            // 幂等检查
            String orderKey = "seckill:order:" + msg.skuId + ":" + msg.userId;
            if (redisTemplate.hasKey(orderKey)) {
                return;
            }

            // 真正落库
            createOrder(msg);

            // 标记订单完成
            redisTemplate.opsForValue().set(orderKey, "1", 24, TimeUnit.HOURS);
        } catch (Exception e) {
            // 业务失败:回滚 Redis 库存
            redisTemplate.opsForValue().increment("seckill:stock:" + msg.skuId);
            log.error("秒杀落库失败", e);
        }
    }
}

五、风控

5.1 设备指纹

javascript
// 浏览器:canvas / WebGL / Audio 等多维度
const fingerprint = {
    userAgent: navigator.userAgent,
    screen: `${screen.width}x${screen.height}`,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    canvas: canvas.toDataURL(),
    plugins: Array.from(navigator.plugins).map(p => p.name).join(',')
};
const hash = sha256(JSON.stringify(fingerprint));

5.2 行为分析

yaml
# 异常特征
- 同一 IP 10 秒内点击 100 次
- 同一设备 1 秒内通过 5 次验证码
- 凌晨 3 点批量下单

# 拦截策略
- 风险分 > 80 直接拒绝
- > 60 出图形验证码
- > 30 限流
java
@Service
public class FraudService {

    public boolean isRisk(String userId, String ip, String deviceId) {
        int score = 0;
        // 1. IP 频次
        if (ipVisitCount(ip) > 50) score += 30;
        // 2. 设备重复
        if (userVisitCount(userId, deviceId) > 3) score += 30;
        // 3. 时间异常
        if (isInBlackHour(LocalTime.now())) score += 20;

        return score > 80;   // 高风险
    }
}

六、Lua 脚本深度优化

6.1 一脚搞定

lua
-- 完整秒杀 Lua
local stockKey = KEYS[1]
local userSetKey = KEYS[2]
local orderQueueKey = KEYS[3]
local skuId = ARGV[1]
local userId = ARGV[2]

-- 检查是否重复购买
if redis.call('sismember', userSetKey, userId) == 1 then
    return -1   -- 重复
end

-- 库存检查
local stock = tonumber(redis.call('get', stockKey))
if not stock or stock <= 0 then
    return 0    -- 售罄
end

-- 扣减
redis.call('decr', stockKey)
redis.call('sadd', userSetKey, userId)

-- 投递订单队列
redis.call('lpush', orderQueueKey, skuId .. ',' .. userId)

return 1    -- 成功

6.2 失败回滚

下游消费失败时,补偿回 Redis:

java
public void rollback(String skuId, String userId) {
    String stockKey = "seckill:stock:" + skuId;
    String userSetKey = "seckill:users:" + skuId;

    redisTemplate.opsForValue().increment(stockKey);
    redisTemplate.opsForSet().remove(userSetKey, userId);
}

七、订单系统合并

订单创建独立服务,30 分钟未支付自动取消(Redis 延迟队列)。

八、性能压测

8.1 关键指标

指标目标
QPS10 万+
P99< 100ms
CPU< 70%
Redis 使用率< 60%
库存差异0

8.2 JMeter 压测

xml
<ThreadGroup>
    <numThreads>1000</numThreads>
    <rampTime>10</rampTime>
    <duration>60</duration>
    <loopCount>10</loopCount>
</ThreadGroup>

8.3 全链路压测

bash
# 流量打标记
wrk -t 16 -c 1000 -d 60s \
    -H "X-Test-Flag: stress-test" \
    http://gateway/seckill/api

压测数据不入库,只读不写。

九、监控大屏

java
// BigScreenMetric 推送指标
ScheduledExecutorService.scheduleAtFixedRate(() -> {
    bigScreen.push("seckill_qps", meterRegistry.find("seckill_requests").counter().count());
    bigScreen.push("stock_remaining", redisTemplate.opsForValue().get("seckill:stock:001"));
}, 0, 1, TimeUnit.SECONDS);

十、回滚机制

10.1 库存预热

java
@Component
public class SeckillWarmup {

    @PostConstruct
    public void warmup() {
        // 启动时把库存加载到 Redis
        seckillSkuRepo.findAll().forEach(sku -> {
            redisTemplate.opsForValue().set(
                "seckill:stock:" + sku.getId(),
                sku.getTotal()
            );
        });
    }
}

10.2 异常熔断

java
@PostConstruct
public void initFlowRule() {
    FlowRule rule = new FlowRule("seckill");
    rule.setCount(50_000);   // 5 万 QPS 上限
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    FlowRuleManager.loadRules(List.of(rule));
}

超过限流阈值,直接返回"SOLD_OUT"。

十一、总结架构

十二、本章小结

主题方案
前端验证码、按钮置灰、URL 随机化
接入层L7 + Nginx + Gateway 逐层限流
业务Redis Lua 原子扣减
异步MQ 削峰、消费者落库
防刷限流、签名、风控
防护熔断、库存补偿、监控

动手练习

设计一个简单的秒杀系统:

  1. 用 Redis Lua 实现原子扣减
  2. 写限流 + 签名校验中间件
  3. 设计 MQ 消费端做异步落库
  4. 用 JMeter 压测,观察瓶颈

下一章:第 234 章:大数据量处理

本站基于 VitePress 构建 · 由 Codebook 团队维护