⚡ 百万TPS秒杀系统:从0到1的架构设计!

36 阅读11分钟

副标题:让1亿用户抢100件商品不崩溃的秘密 🎯


🎬 开场:秒杀的魔力与挑战

真实案例

小米手机秒杀:

活动时间:2023年12月12日 10:00:00
商品库存:1000台
参与人数:500万
实际TPS:200万+

技术挑战:
├── 瞬时流量:平时100倍
├── 库存有限:超卖风险
├── 恶意刷单:黄牛党
└── 用户体验:响应超时

结果:
✅ 系统稳定
✅ 0.3秒售罄
✅ 无超卖
✅ 无宕机

秒杀的核心挑战

挑战1:瞬时高并发 🔥
平时:1000 QPS
秒杀:1000000 QPS
增长:1000倍!

挑战2:库存有限 📦
商品:100件
请求:1000000次
比例:1:10000

挑战3:读多写少 📊
读操作:查询商品详情、库存
写操作:扣减库存、创建订单
比例:10000:1

挑战4:恶意攻击 🤖
黄牛党:
├── 脚本刷单
├── 分布式攻击
└── 恶意占用资源

🏗️ 系统架构设计

整体架构图

┌──────────────────────────────────────────┐
│            CDN + 静态资源                │
└────────────┬─────────────────────────────┘
             │
┌────────────▼─────────────────────────────┐
│       Nginx(限流 + 负载均衡)           │
└────────────┬─────────────────────────────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼────┐      ┌────▼───┐
│ 网关层 │      │ 网关层 │
│ (限流) │      │ (限流) │
└───┬────┘      └────┬───┘
    │                │
    └────────┬────────┘
             │
┌────────────▼─────────────────────────────┐
│          秒杀服务集群(无状态)           │
│  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐    │
│  │实例1│  │实例2│  │实例3│  │实例4│    │
│  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘    │
└─────┼──────┼──────┼──────┼─────────────┘
      │      │      │      │
      └──────┴──────┴──────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼────┐      ┌────▼────┐
│ Redis  │      │  MySQL  │
│ 集群   │      │  集群   │
└────────┘      └─────────┘
    │                 │
    └────────┬────────┘
             │
    ┌────────▼────────┐
    │   消息队列MQ    │
    │   (异步处理)    │
    └─────────────────┘

📝 详细设计方案

阶段1:前端优化

1. 静态资源CDN

<!-- 秒杀页面 -->
<!DOCTYPE html>
<html>
<head>
    <title>小米手机秒杀</title>
    
    <!-- CSS和JS使用CDN -->
    <link rel="stylesheet" 
          href="https://cdn.example.com/seckill.css?v=20231212">
    <script src="https://cdn.example.com/seckill.js?v=20231212"></script>
</head>
<body>
    <!-- 商品图片使用CDN -->
    <img src="https://cdn.example.com/product/xiaomi14.jpg" 
         alt="小米14">
    
    <!-- 商品详情(静态数据) -->
    <div class="product-info">
        <h1>小米14 手机</h1>
        <p class="price">¥3999</p>
        <p class="desc">骁龙8 Gen3 | 徕卡影像 | 120W快充</p>
    </div>
    
    <!-- 秒杀按钮(只有这个是动态的) -->
    <div class="seckill-action">
        <button id="seckillBtn" onclick="doSeckill()">
            立即抢购
        </button>
        <span id="countdown">距离开始:00:05:30</span>
    </div>
</body>
</html>

2. 按钮控制

/**
 * 前端防重复提交
 */
class SeckillController {
    constructor() {
        this.clicked = false;
        this.token = null;
    }
    
    /**
     * 秒杀按钮点击
     */
    async doSeckill() {
        // 1. 防止重复点击
        if (this.clicked) {
            alert('请勿重复提交!');
            return;
        }
        
        // 2. 标记已点击
        this.clicked = true;
        const btn = document.getElementById('seckillBtn');
        btn.disabled = true;
        btn.innerText = '抢购中...';
        
        // 3. 获取秒杀令牌
        if (!this.token) {
            this.token = await this.getSeckillToken();
            if (!this.token) {
                this.reset();
                alert('获取令牌失败,请重试');
                return;
            }
        }
        
        // 4. 发起秒杀请求
        try {
            const result = await this.requestSeckill(this.token);
            
            if (result.success) {
                alert('抢购成功!请尽快支付');
                window.location.href = '/order/pay?orderId=' + result.orderId;
            } else {
                alert(result.message || '抢购失败');
                this.reset();
            }
            
        } catch (error) {
            console.error('秒杀失败', error);
            alert('网络异常,请重试');
            this.reset();
        }
    }
    
    /**
     * 获取秒杀令牌(限流)
     */
    async getSeckillToken() {
        try {
            const response = await fetch('/api/seckill/token', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    activityId: 12345,
                    userId: this.getUserId()
                })
            });
            
            const data = await response.json();
            return data.token;
            
        } catch (error) {
            console.error('获取令牌失败', error);
            return null;
        }
    }
    
    /**
     * 发起秒杀请求
     */
    async requestSeckill(token) {
        const response = await fetch('/api/seckill/execute', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Seckill-Token': token
            },
            body: JSON.stringify({
                activityId: 12345,
                userId: this.getUserId()
            })
        });
        
        return await response.json();
    }
    
    /**
     * 重置状态
     */
    reset() {
        this.clicked = false;
        const btn = document.getElementById('seckillBtn');
        btn.disabled = false;
        btn.innerText = '立即抢购';
    }
    
    getUserId() {
        return localStorage.getItem('userId');
    }
}

3. 倒计时和预加载

/**
 * 倒计时控制
 */
class CountdownTimer {
    constructor(startTime) {
        this.startTime = new Date(startTime).getTime();
        this.timer = null;
    }
    
    /**
     * 开始倒计时
     */
    start() {
        this.timer = setInterval(() => {
            const now = Date.now();
            const diff = this.startTime - now;
            
            if (diff <= 0) {
                // 秒杀开始
                this.onStart();
                clearInterval(this.timer);
            } else {
                // 更新倒计时显示
                this.updateDisplay(diff);
                
                // 提前1秒预加载
                if (diff <= 1000 && !this.preloaded) {
                    this.preload();
                    this.preloaded = true;
                }
            }
        }, 100);
    }
    
    /**
     * 更新倒计时显示
     */
    updateDisplay(diff) {
        const hours = Math.floor(diff / (1000 * 60 * 60));
        const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
        const seconds = Math.floor((diff % (1000 * 60)) / 1000);
        
        document.getElementById('countdown').innerText = 
            `距离开始:${this.pad(hours)}:${this.pad(minutes)}:${this.pad(seconds)}`;
    }
    
    /**
     * 预加载(提前建立连接)
     */
    async preload() {
        console.log('预加载秒杀接口...');
        
        // 预热接口(HEAD请求)
        fetch('/api/seckill/warmup', { method: 'HEAD' });
        
        // 预获取令牌
        const controller = new SeckillController();
        await controller.getSeckillToken();
    }
    
    /**
     * 秒杀开始
     */
    onStart() {
        document.getElementById('countdown').innerText = '秒杀进行中';
        document.getElementById('seckillBtn').disabled = false;
        document.getElementById('seckillBtn').classList.add('active');
    }
    
    pad(num) {
        return num < 10 ? '0' + num : num;
    }
}

// 初始化
const timer = new CountdownTimer('2023-12-12 10:00:00');
timer.start();

阶段2:网关层限流

1. Nginx限流

# nginx.conf

http {
    # 限流配置
    # 每个IP每秒最多10个请求
    limit_req_zone $binary_remote_addr zone=seckill_limit:10m rate=10r/s;
    
    # 每个IP最多保持50个连接
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
    server {
        listen 80;
        server_name seckill.example.com;
        
        location /api/seckill/ {
            # 应用限流规则
            limit_req zone=seckill_limit burst=20 nodelay;
            limit_conn conn_limit 50;
            
            # 返回友好的限流提示
            limit_req_status 429;
            limit_conn_status 429;
            
            # 代理到后端
            proxy_pass http://seckill_backend;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # 超时设置
            proxy_connect_timeout 3s;
            proxy_send_timeout 3s;
            proxy_read_timeout 3s;
        }
    }
    
    # 后端服务器集群
    upstream seckill_backend {
        server 192.168.1.101:8080 weight=1 max_fails=2 fail_timeout=30s;
        server 192.168.1.102:8080 weight=1 max_fails=2 fail_timeout=30s;
        server 192.168.1.103:8080 weight=1 max_fails=2 fail_timeout=30s;
        server 192.168.1.104:8080 weight=1 max_fails=2 fail_timeout=30s;
        
        keepalive 1000;
    }
}

2. 网关层令牌桶限流

/**
 * API网关限流
 */
@Component
public class SeckillGatewayFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 总限流:每秒100万请求
    private static final long TOTAL_PERMITS_PER_SECOND = 1000000;
    
    // 单用户限流:每秒10个请求
    private static final long USER_PERMITS_PER_SECOND = 10;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        
        // 只对秒杀接口限流
        if (!path.startsWith("/api/seckill/")) {
            return chain.filter(exchange);
        }
        
        // 1. 全局限流
        if (!tryAcquireGlobal()) {
            return responseLimit(exchange, "系统繁忙,请稍后重试");
        }
        
        // 2. 用户级限流
        String userId = getUserId(exchange);
        if (userId != null && !tryAcquireUser(userId)) {
            return responseLimit(exchange, "您的操作太频繁,请稍后重试");
        }
        
        return chain.filter(exchange);
    }
    
    /**
     * 全局限流(令牌桶)
     */
    private boolean tryAcquireGlobal() {
        String key = "seckill:limit:global";
        
        // 使用Redis + Lua实现令牌桶
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local capacity = tonumber(ARGV[1])\n" +
            "local rate = tonumber(ARGV[2])\n" +
            "local now = tonumber(ARGV[3])\n" +
            "\n" +
            "local tokens = redis.call('hget', key, 'tokens')\n" +
            "local last_time = redis.call('hget', key, 'last_time')\n" +
            "\n" +
            "if tokens == false then\n" +
            "  tokens = capacity\n" +
            "  last_time = now\n" +
            "else\n" +
            "  tokens = tonumber(tokens)\n" +
            "  last_time = tonumber(last_time)\n" +
            "end\n" +
            "\n" +
            "local elapsed = math.max(0, now - last_time)\n" +
            "local new_tokens = math.min(capacity, tokens + elapsed * rate)\n" +
            "\n" +
            "if new_tokens >= 1 then\n" +
            "  new_tokens = new_tokens - 1\n" +
            "  redis.call('hset', key, 'tokens', new_tokens)\n" +
            "  redis.call('hset', key, 'last_time', now)\n" +
            "  redis.call('expire', key, 60)\n" +
            "  return 1\n" +
            "else\n" +
            "  return 0\n" +
            "end";
        
        RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
        
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList(key),
            String.valueOf(TOTAL_PERMITS_PER_SECOND),  // capacity
            String.valueOf(TOTAL_PERMITS_PER_SECOND),  // rate (每秒补充)
            String.valueOf(System.currentTimeMillis() / 1000)  // now
        );
        
        return result != null && result == 1;
    }
    
    /**
     * 用户级限流
     */
    private boolean tryAcquireUser(String userId) {
        String key = "seckill:limit:user:" + userId;
        
        // 使用滑动窗口限流
        long now = System.currentTimeMillis();
        long windowStart = now - 1000;  // 1秒窗口
        
        // 移除过期的记录
        redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
        
        // 统计当前窗口内的请求数
        Long count = redisTemplate.opsForZSet().zCard(key);
        
        if (count != null && count >= USER_PERMITS_PER_SECOND) {
            return false;
        }
        
        // 添加当前请求
        redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
        redisTemplate.expire(key, 2, TimeUnit.SECONDS);
        
        return true;
    }
    
    /**
     * 返回限流响应
     */
    private Mono<Void> responseLimit(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        String body = "{\"code\":429,\"message\":\"" + message + "\"}";
        DataBuffer buffer = exchange.getResponse().bufferFactory()
            .wrap(body.getBytes(StandardCharsets.UTF_8));
        
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
    
    @Override
    public int getOrder() {
        return -100;  // 优先级最高
    }
}

阶段3:库存预热

/**
 * 库存预热到Redis
 */
@Service
public class StockWarmUpService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private SeckillActivityMapper activityMapper;
    
    /**
     * 秒杀开始前预热库存
     */
    public void warmUpStock(Long activityId) {
        // 1. 从数据库加载活动信息
        SeckillActivity activity = activityMapper.selectById(activityId);
        
        if (activity == null) {
            throw new RuntimeException("活动不存在");
        }
        
        // 2. 预热库存到Redis
        String stockKey = "seckill:stock:" + activityId;
        redisTemplate.opsForValue().set(stockKey, activity.getStock());
        
        // 3. 设置过期时间(活动结束后1小时)
        long ttl = activity.getEndTime().getTime() - System.currentTimeMillis() + 3600000;
        redisTemplate.expire(stockKey, ttl, TimeUnit.MILLISECONDS);
        
        log.info("库存预热完成: activityId={}, stock={}", activityId, activity.getStock());
        
        // 4. 预热商品信息
        String productKey = "seckill:product:" + activityId;
        redisTemplate.opsForValue().set(productKey, activity.getProduct());
        redisTemplate.expire(productKey, ttl, TimeUnit.MILLISECONDS);
        
        log.info("商品信息预热完成: activityId={}", activityId);
    }
    
    /**
     * 批量预热多个活动
     */
    @Scheduled(cron = "0 0 9 * * ?")  // 每天9点执行
    public void batchWarmUp() {
        // 查询今天的秒杀活动
        List<SeckillActivity> activities = activityMapper.selectTodayActivities();
        
        log.info("开始预热今日秒杀活动,共{}个", activities.size());
        
        for (SeckillActivity activity : activities) {
            try {
                warmUpStock(activity.getId());
            } catch (Exception e) {
                log.error("预热失败: activityId={}", activity.getId(), e);
            }
        }
        
        log.info("预热完成");
    }
}

阶段4:核心秒杀逻辑

1. 秒杀令牌

/**
 * 秒杀令牌服务
 */
@Service
public class SeckillTokenService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 生成秒杀令牌(限流)
     */
    public String generateToken(Long activityId, Long userId) {
        // 1. 检查用户是否已经有令牌
        String userTokenKey = String.format("seckill:token:user:%d:%d", 
            activityId, userId);
        
        String existingToken = redisTemplate.opsForValue().get(userTokenKey);
        if (existingToken != null) {
            return existingToken;
        }
        
        // 2. 检查令牌总数是否超限
        String tokenCountKey = "seckill:token:count:" + activityId;
        Long count = redisTemplate.opsForValue().increment(tokenCountKey);
        
        // 令牌数量 = 库存 * 5 (超卖保护)
        long maxTokens = getStockCount(activityId) * 5;
        
        if (count > maxTokens) {
            throw new SeckillException("活动太火爆,请稍后再试");
        }
        
        // 3. 生成令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        
        // 4. 存储令牌
        String tokenKey = "seckill:token:" + token;
        Map<String, Object> tokenData = new HashMap<>();
        tokenData.put("activityId", activityId);
        tokenData.put("userId", userId);
        tokenData.put("createTime", System.currentTimeMillis());
        
        redisTemplate.opsForHash().putAll(tokenKey, tokenData);
        redisTemplate.expire(tokenKey, 5, TimeUnit.MINUTES);  // 5分钟有效
        
        // 5. 关联用户
        redisTemplate.opsForValue().set(userTokenKey, token, 5, TimeUnit.MINUTES);
        
        log.info("生成秒杀令牌: token={}, userId={}", token, userId);
        
        return token;
    }
    
    /**
     * 验证令牌
     */
    public boolean validateToken(String token, Long activityId, Long userId) {
        String tokenKey = "seckill:token:" + token;
        
        Map<Object, Object> tokenData = redisTemplate.opsForHash().entries(tokenKey);
        
        if (tokenData.isEmpty()) {
            return false;
        }
        
        Long tokenActivityId = Long.valueOf(tokenData.get("activityId").toString());
        Long tokenUserId = Long.valueOf(tokenData.get("userId").toString());
        
        return tokenActivityId.equals(activityId) && tokenUserId.equals(userId);
    }
    
    /**
     * 消费令牌(一次性)
     */
    public void consumeToken(String token) {
        String tokenKey = "seckill:token:" + token;
        redisTemplate.delete(tokenKey);
    }
    
    private Long getStockCount(Long activityId) {
        String stockKey = "seckill:stock:" + activityId;
        Object stock = redisTemplate.opsForValue().get(stockKey);
        return stock != null ? Long.valueOf(stock.toString()) : 0L;
    }
}

2. 库存扣减(Lua脚本)

/**
 * 库存扣减服务
 */
@Service
public class StockDeductionService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 扣减库存(原子操作)
     */
    public boolean deductStock(Long activityId, Long userId) {
        String stockKey = "seckill:stock:" + activityId;
        String userKey = "seckill:user:" + activityId + ":" + userId;
        
        // Lua脚本保证原子性
        String luaScript = 
            "-- 检查用户是否已经购买\n" +
            "if redis.call('exists', KEYS[2]) == 1 then\n" +
            "  return -2  -- 已经购买过\n" +
            "end\n" +
            "\n" +
            "-- 获取当前库存\n" +
            "local stock = redis.call('get', KEYS[1])\n" +
            "if not stock or tonumber(stock) <= 0 then\n" +
            "  return -1  -- 库存不足\n" +
            "end\n" +
            "\n" +
            "-- 扣减库存\n" +
            "redis.call('decr', KEYS[1])\n" +
            "\n" +
            "-- 标记用户已购买\n" +
            "redis.call('setex', KEYS[2], 3600, '1')\n" +
            "\n" +
            "return 1  -- 成功";
        
        RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
        
        Long result = redisTemplate.execute(
            script,
            Arrays.asList(stockKey, userKey)
        );
        
        if (result == null) {
            return false;
        }
        
        switch (result.intValue()) {
            case 1:
                log.info("扣减库存成功: activityId={}, userId={}", activityId, userId);
                return true;
            case -1:
                log.warn("库存不足: activityId={}", activityId);
                throw new SeckillException("商品已售罄");
            case -2:
                log.warn("重复购买: activityId={}, userId={}", activityId, userId);
                throw new SeckillException("您已经购买过了");
            default:
                return false;
        }
    }
}

3. 秒杀主流程

/**
 * 秒杀服务
 */
@Service
public class SeckillService {
    
    @Autowired
    private SeckillTokenService tokenService;
    
    @Autowired
    private StockDeductionService stockService;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 执行秒杀
     */
    @Transactional(rollbackFor = Exception.class)
    public SeckillResult executeSeckill(SeckillRequest request) {
        Long activityId = request.getActivityId();
        Long userId = request.getUserId();
        String token = request.getToken();
        
        // 1. 验证令牌
        if (!tokenService.validateToken(token, activityId, userId)) {
            throw new SeckillException("令牌无效");
        }
        
        // 2. 检查活动状态
        if (!isActivityValid(activityId)) {
            throw new SeckillException("活动已结束");
        }
        
        // 3. 扣减库存(Redis原子操作)
        boolean success = stockService.deductStock(activityId, userId);
        
        if (!success) {
            return SeckillResult.fail("抢购失败");
        }
        
        // 4. 消费令牌
        tokenService.consumeToken(token);
        
        // 5. 发送MQ消息(异步创建订单)
        SeckillMessage message = new SeckillMessage();
        message.setActivityId(activityId);
        message.setUserId(userId);
        message.setTimestamp(System.currentTimeMillis());
        
        rabbitTemplate.convertAndSend(
            "seckill.order.exchange",
            "seckill.order.create",
            message
        );
        
        log.info("秒杀成功,发送MQ消息: userId={}, activityId={}", 
            userId, activityId);
        
        // 6. 返回结果
        return SeckillResult.success("抢购成功,订单生成中");
    }
    
    /**
     * 检查活动是否有效
     */
    private boolean isActivityValid(Long activityId) {
        String key = "seckill:activity:" + activityId;
        
        // 从缓存获取活动信息
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached == null) {
            // 缓存未命中,查数据库
            // ...
        }
        
        // 检查活动时间
        // ...
        
        return true;
    }
}

/**
 * 秒杀Controller
 */
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    @Autowired
    private SeckillTokenService tokenService;
    
    /**
     * 获取秒杀令牌
     */
    @PostMapping("/token")
    public Result<String> getToken(@RequestBody TokenRequest request) {
        Long userId = request.getUserId();
        Long activityId = request.getActivityId();
        
        // 参数校验
        if (userId == null || activityId == null) {
            return Result.fail("参数错误");
        }
        
        try {
            String token = tokenService.generateToken(activityId, userId);
            return Result.success(token);
            
        } catch (SeckillException e) {
            return Result.fail(e.getMessage());
        }
    }
    
    /**
     * 执行秒杀
     */
    @PostMapping("/execute")
    public Result<SeckillResult> execute(
        @RequestBody SeckillRequest request,
        @RequestHeader("X-Seckill-Token") String token) {
        
        request.setToken(token);
        
        try {
            SeckillResult result = seckillService.executeSeckill(request);
            return Result.success(result);
            
        } catch (SeckillException e) {
            return Result.fail(e.getMessage());
        }
    }
}

阶段5:异步订单处理

/**
 * 订单消息消费者
 */
@Component
public class OrderMessageConsumer {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private StockMapper stockMapper;
    
    /**
     * 消费秒杀订单消息
     */
    @RabbitListener(queues = "seckill.order.queue")
    public void handleOrderMessage(SeckillMessage message) {
        Long activityId = message.getActivityId();
        Long userId = message.getUserId();
        
        log.info("收到秒杀订单消息: userId={}, activityId={}", userId, activityId);
        
        try {
            // 1. 创建订单
            Order order = orderService.createSeckillOrder(activityId, userId);
            
            // 2. 扣减数据库库存
            int rows = stockMapper.deductStock(activityId, 1);
            
            if (rows == 0) {
                // 库存扣减失败,回滚订单
                log.error("数据库库存扣减失败: activityId={}", activityId);
                orderService.cancelOrder(order.getId());
                return;
            }
            
            // 3. 发送订单创建通知
            notifyOrderCreated(order);
            
            log.info("订单创建成功: orderId={}, userId={}", order.getId(), userId);
            
        } catch (Exception e) {
            log.error("处理秒杀订单失败: userId={}, activityId={}", 
                userId, activityId, e);
            
            // 回滚Redis库存
            rollbackRedisStock(activityId);
        }
    }
    
    /**
     * 回滚Redis库存
     */
    private void rollbackRedisStock(Long activityId) {
        String stockKey = "seckill:stock:" + activityId;
        redisTemplate.opsForValue().increment(stockKey);
        
        log.info("回滚Redis库存: activityId={}", activityId);
    }
    
    /**
     * 发送订单通知
     */
    private void notifyOrderCreated(Order order) {
        // 发送短信/推送通知
        // ...
    }
}

🎯 防刷和安全

1. 验证码

/**
 * 验证码服务
 */
@Service
public class CaptchaService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 生成图形验证码
     */
    public CaptchaVO generateCaptcha(String sessionId) {
        // 生成随机验证码
        String code = RandomStringUtils.randomNumeric(4);
        
        // 生成图片
        BufferedImage image = createImage(code);
        
        // 保存到Redis
        String key = "captcha:" + sessionId;
        redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
        
        // 转换为Base64
        String imageBase64 = imageToBase64(image);
        
        CaptchaVO vo = new CaptchaVO();
        vo.setSessionId(sessionId);
        vo.setImage(imageBase64);
        
        return vo;
    }
    
    /**
     * 验证验证码
     */
    public boolean validateCaptcha(String sessionId, String userCode) {
        String key = "captcha:" + sessionId;
        String correctCode = redisTemplate.opsForValue().get(key);
        
        if (correctCode == null) {
            return false;
        }
        
        // 验证后删除
        redisTemplate.delete(key);
        
        return correctCode.equalsIgnoreCase(userCode);
    }
}

2. 黑名单

/**
 * 黑名单服务
 */
@Service
public class BlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 检查用户是否在黑名单
     */
    public boolean isBlacklisted(Long userId) {
        String key = "blacklist:user:" + userId;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    /**
     * 加入黑名单
     */
    public void addToBlacklist(Long userId, long hours) {
        String key = "blacklist:user:" + userId;
        redisTemplate.opsForValue().set(key, "1", hours, TimeUnit.HOURS);
        
        log.warn("用户加入黑名单: userId={}, hours={}", userId, hours);
    }
    
    /**
     * 检测异常行为
     */
    @Async
    public void detectAbnormalBehavior(Long userId) {
        // 检查用户最近的请求频率
        String countKey = "behavior:count:" + userId;
        Long count = redisTemplate.opsForValue().increment(countKey);
        
        if (count == 1) {
            redisTemplate.expire(countKey, 60, TimeUnit.SECONDS);
        }
        
        // 1分钟内超过100次请求,加入黑名单
        if (count > 100) {
            addToBlacklist(userId, 24);  // 封禁24小时
            
            // 告警
            alertService.send("检测到异常行为", 
                "用户 " + userId + " 1分钟内请求" + count + "次");
        }
    }
}

📊 监控和统计

/**
 * 秒杀监控服务
 */
@Service
public class SeckillMonitorService {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 记录秒杀请求
     */
    public void recordRequest(Long activityId, boolean success) {
        Counter.builder("seckill.request")
            .tag("activityId", String.valueOf(activityId))
            .tag("success", String.valueOf(success))
            .register(meterRegistry)
            .increment();
    }
    
    /**
     * 实时统计
     */
    @Scheduled(fixedDelay = 1000)  // 每秒
    public void realTimeStats() {
        List<Long> activityIds = getActiveActivityIds();
        
        for (Long activityId : activityIds) {
            // 剩余库存
            Long stock = getRemainStock(activityId);
            
            // 成功订单数
            Long successCount = getSuccessCount(activityId);
            
            // 请求总数
            Long totalRequest = getTotalRequest(activityId);
            
            // 成功率
            double successRate = totalRequest > 0 ? 
                (double) successCount / totalRequest * 100 : 0;
            
            log.info("秒杀实时数据: activityId={}, stock={}, success={}, total={}, rate={}%",
                activityId, stock, successCount, totalRequest, 
                String.format("%.2f", successRate));
            
            // 发送到监控系统
            Gauge.builder("seckill.stock", stock::doubleValue)
                .tag("activityId", String.valueOf(activityId))
                .register(meterRegistry);
        }
    }
    
    private Long getRemainStock(Long activityId) {
        String key = "seckill:stock:" + activityId;
        Object value = redisTemplate.opsForValue().get(key);
        return value != null ? Long.valueOf(value.toString()) : 0L;
    }
}

🎉 总结

架构核心要点

1. 前端优化 🌐
   ├── CDN加速
   ├── 静态资源
   ├── 按钮防重
   └── 倒计时预热

2. 限流层 🚦
   ├── Nginx限流
   ├── 网关限流
   ├── 令牌桶
   └── 滑动窗口

3. 缓存层 ⚡
   ├── 库存预热
   ├── Redis扣减
   ├── Lua脚本
   └── 原子操作

4. 异步层 📮
   ├── MQ削峰
   ├── 异步订单
   ├── 批量处理
   └── 最终一致性

5. 安全层 🛡️
   ├── 令牌验证
   ├── 验证码
   ├── 黑名单
   └── 行为检测

性能指标

目标:
├── 支持TPS:100万+
├── 响应时间:< 500ms
├── 成功率:> 99.9%
└── 0超卖

实现:
├── Nginx:过滤90%无效请求
├── 网关:再过滤80%
├── Redis:承载100万QPS
├── MQ:削峰填谷
└── DB:只承载成功订单

记忆口诀

秒杀系统要设计,
分层架构是关键。

前端优化CDN快,
静态资源不变化。
按钮防重加令牌,
倒计时预热连接。

限流层次有三级,
Nginx网关加应用。
令牌桶和滑动窗,
黑名单来防刷单。

库存预热到Redis,
Lua脚本保原子。
扣减成功发MQ,
异步处理创订单。

监控告警要实时,
随时准备应急案。
百万TPS不是梦,
架构合理就能行!

愿你的秒杀系统永不宕机,订单创建秒级完成! ⚡✨