🔥 秒杀系统设计完全指南:10年老Java的血泪总结 🔥

83 阅读5分钟

写在前面:老哥我做了10年Java,双十一扛过N次秒杀系统,被老板骂过,被运维打过,被DBA追着跑过... 今天把这些年的经验全部吐血整理出来,保证让你看完就懂,学完就会!💪


📋 目录

  1. 开场白:秒杀有多可怕?
  2. 核心挑战:三座大山
  3. 设计原则:三个字的真理
  4. 详细设计方案
  5. 代码实战
  6. 上线前的自检清单
  7. 总结

1. 开场白:秒杀有多可怕? 😱

想象一下这个画面:

凌晨00:00:00,你的秒杀系统上线了。商品是iPhone 16 Pro Max,原价9999,秒杀价1元,库存1000台。

00:00:01,全国100万用户同时点击"立即抢购"

00:00:02,你的服务器CPU 100%,内存爆了

00:00:03,数据库挂了,Redis也挂了

00:00:04,老板电话打来:"小王啊,系统怎么挂了?"

00:00:05,你开始怀疑人生:"我为什么要做程序员?" 😭

这就是没有设计好秒杀系统的下场!

🎯 用生活例子理解秒杀

秒杀系统就像是:

  • 春运抢票 🚄:几亿人同时抢几万张票
  • 明星演唱会抢票 🎤:百万粉丝抢几千张票
  • 超市限时抢购 🛒:开门瞬间冲进去抢打折鸡蛋

如果没有合理的设计,结果就是:

  • 人挤人,大家都进不去(系统崩溃)
  • 有人拿了10个鸡蛋,结果只有5个(超卖)
  • 排队排了半天,被告知没货了(用户体验差)

2. 核心挑战:三座大山 ⛰️

2.1 第一座山:高并发 🌊

场景:1000个商品,100万用户,读写比例是 1000:1

  • 正常电商:1秒内可能只有100个请求
  • 秒杀场景:1秒内有100万个请求!

这就像是:

正常情况:一个水龙头慢慢流水 💧
秒杀情况:消防车的水枪狂喷 🚒💦💦💦

2.2 第二座山:超卖问题 📉

超卖就是:库存只有1000,但卖出去了1100个!

为什么会超卖?

时刻T1: 用户A查询库存 = 1
时刻T2: 用户B查询库存 = 1  
时刻T3: 用户A扣减库存,剩余 = 0
时刻T4: 用户B扣减库存,剩余 = -1  ❌ 完蛋!

这就像超市收银员:

  • 收银员A看到货架上还有1个鸡蛋
  • 收银员B也看到货架上还有1个鸡蛋
  • 结果两个人都卖了出去,实际只有1个!

2.3 第三座山:用户体验 😤

用户最怕的是:

  • ❌ 点了半天没反应
  • ❌ 转圈圈转了5分钟
  • ❌ 页面白屏死机
  • ❌ 提示"系统繁忙,请稍后再试"(然后商品没了)

3. 设计原则:三个字的真理 🎯

✅ 稳(高可用)

系统不能挂!宁可慢一点,也不能崩!

✅ 准(一致性)

库存扣减必须准确,不能超卖,也不能少卖!

✅ 快(高性能)

响应要快,用户等不及!


4. 详细设计方案 🏗️

4.1 整体架构图

                        ┌─────────────────┐
                        │   用户浏览器    │
                        └────────┬────────┘
                                 │
                    ┌────────────▼───────────┐
                    │      CDN (静态资源)     │
                    └────────────┬───────────┘
                                 │
                    ┌────────────▼───────────┐
                    │     Nginx (负载均衡)    │
                    └────────────┬───────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              │                  │                  │
    ┌─────────▼────────┐ ┌──────▼──────┐ ┌────────▼────────┐
    │ 秒杀服务器-1     │ │ 秒杀服务器-2│ │ 秒杀服务器-N    │
    └─────────┬────────┘ └──────┬──────┘ └────────┬────────┘
              │                  │                  │
              └──────────────────┼──────────────────┘
                                 │
                    ┌────────────▼───────────┐
                    │   Redis集群 (热数据)   │
                    └────────────┬───────────┘
                                 │
                    ┌────────────▼───────────┐
                    │   消息队列 (削峰)      │
                    └────────────┬───────────┘
                                 │
                    ┌────────────▼───────────┐
                    │  MySQL主从 (持久化)    │
                    └────────────────────────┘

4.2 第一招:前端防刷 🛡️

目标:挡住99%的无效请求

4.2.1 按钮置灰

// 用户点击后,按钮立即置灰5秒
let isClicking = false;

function seckillClick() {
    if (isClicking) {
        alert('您点击太快啦,休息一下~');
        return;
    }
    
    isClicking = true;
    document.getElementById('seckillBtn').disabled = true;
    
    // 5秒后恢复
    setTimeout(() => {
        isClicking = false;
        document.getElementById('seckillBtn').disabled = false;
    }, 5000);
    
    // 发送秒杀请求
    sendSeckillRequest();
}

生活例子:就像电梯按钮,按了一次就会亮灯,短时间内按多少次都没用!

4.2.2 验证码

<!-- 秒杀前先弹出验证码 -->
<div id="captchaModal">
    <img src="/captcha/generate" />
    <input type="text" placeholder="请输入验证码" />
</div>

作用

  • 防止机器人刷单 🤖
  • 把100万请求分散到10秒内(让用户输入验证码需要时间)
  • 一个验证码就能挡住80%的流量!

4.3 第二招:Nginx限流 🚦

目标:控制流入后端的请求数量

# nginx.conf
http {
    # 限流:每个IP每秒只允许10个请求
    limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
    
    server {
        location /api/seckill {
            # 超过限制的请求直接返回503
            limit_req zone=seckill burst=20 nodelay;
            limit_req_status 503;
            
            proxy_pass http://backend;
        }
    }
}

生活例子:就像高速公路的收费站,控制单位时间内通过的车辆数量!

效果对比

没有限流:100万请求全部打到后端 → 后端崩溃 💥
有了限流:每秒只放1万请求进来 → 后端稳如老狗 🐕

4.4 第三招:Redis缓存库存 ⚡

核心思路:把库存放在内存里,不查数据库!

4.4.1 初始化库存

/**
 * 系统启动时,把库存从MySQL加载到Redis
 */
@PostConstruct
public void initStock() {
    // 假设商品ID=1001,库存1000
    String stockKey = "seckill:stock:1001";
    redisTemplate.opsForValue().set(stockKey, "1000");
    
    System.out.println("✅ 库存初始化完成!");
}

4.4.2 扣减库存(重点!)

方式一:简单版(有超卖风险)❌

// ❌ 错误示范!千万别这么写!
public boolean wrongDecrStock(Long productId) {
    String stockKey = "seckill:stock:" + productId;
    
    // 问题:这三步不是原子操作!
    Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
    if (stock > 0) {
        redisTemplate.opsForValue().set(stockKey, stock - 1);
        return true;
    }
    return false;
}

问题分析

时刻T1: 线程A读取库存 = 1
时刻T2: 线程B读取库存 = 1
时刻T3: 线程A写入库存 = 0
时刻T4: 线程B写入库存 = 0  ← 两个人都成功了!超卖!

方式二:Redis原子操作(推荐)✅

/**
 * 使用Redis的原子操作扣减库存
 */
public boolean decrStockAtomic(Long productId) {
    String stockKey = "seckill:stock:" + productId;
    
    // decrement是原子操作,不会超卖!
    Long stock = redisTemplate.opsForValue().decrement(stockKey);
    
    if (stock >= 0) {
        System.out.println("✅ 扣减成功,剩余库存:" + stock);
        return true;
    } else {
        // 库存不足,回滚
        redisTemplate.opsForValue().increment(stockKey);
        System.out.println("❌ 库存不足");
        return false;
    }
}

方式三:Lua脚本(最安全)🔒

/**
 * 使用Lua脚本,保证原子性 + 可靠性
 */
public boolean decrStockLua(Long productId) {
    String stockKey = "seckill:stock:" + productId;
    
    String luaScript = 
        "local stock = redis.call('get', KEYS[1]) " +
        "if tonumber(stock) > 0 then " +
        "    redis.call('decr', KEYS[1]) " +
        "    return 1 " +
        "else " +
        "    return 0 " +
        "end";
    
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(luaScript);
    redisScript.setResultType(Long.class);
    
    Long result = redisTemplate.execute(
        redisScript, 
        Collections.singletonList(stockKey)
    );
    
    return result != null && result == 1;
}

为什么Lua脚本最好?

  1. 原子性:整个脚本作为一个整体执行,不会被打断
  2. 性能好:减少网络往返次数
  3. 逻辑清晰:判断和扣减在一起

生活例子

  • 方式一:你数钱的时候别人也在数,数完一起放回去,结果少了 ❌
  • 方式二:你数钱的时候别人不能碰 ✅
  • 方式三:你找个保险柜,把钱锁里面数 ✅✅

4.5 第四招:消息队列削峰 📮

问题:即使用了Redis,还是有10万请求同时扣库存成功,怎么办?

答案:用消息队列异步处理订单!

4.5.1 架构图

  用户请求                Redis扣库存               消息队列              订单服务
     │                       │                        │                     │
     │  ─────────────────>   │                        │                     │
     │                       │                        │                     │
     │  <─────────────────   │                        │                     │
     │   (立即返回:排队中)   │                        │                     │
     │                       │                        │                     │
     │                       │  ─────────────────>    │                     │
     │                       │  (扣减成功,发消息)     │                     │
     │                       │                        │                     │
     │                       │                        │  ───────────────>   │
     │                       │                        │   (消费消息)         │
     │                       │                        │                     │
     │                       │                        │                     │  创建订单
     │                       │                        │                     │  扣减DB库存
     │                       │                        │   <─────────────    │
     │                       │                        │   (处理完成)         │
     │  <──────────────────────────────────────────────────────────────────  │
     │   (推送结果:秒杀成功/失败)                                               │

4.5.2 代码实现

Step 1: 秒杀接口

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 秒杀接口
     */
    @PostMapping("/execute")
    public Result seckill(@RequestParam Long productId, 
                          @RequestParam Long userId) {
        
        // 1. 参数校验
        if (productId == null || userId == null) {
            return Result.fail("参数错误");
        }
        
        // 2. 检查是否重复购买
        String userKey = "seckill:user:" + productId + ":" + userId;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(userKey))) {
            return Result.fail("您已经参加过秒杀了,不能重复购买哦~");
        }
        
        // 3. Redis扣减库存(使用Lua脚本)
        boolean success = decrStockLua(productId);
        if (!success) {
            return Result.fail("商品已售罄,下次早点来哦~");
        }
        
        // 4. 标记用户已参与
        redisTemplate.opsForValue().set(userKey, "1", 24, TimeUnit.HOURS);
        
        // 5. 发送消息到MQ(异步创建订单)
        SeckillMessage message = new SeckillMessage();
        message.setProductId(productId);
        message.setUserId(userId);
        message.setTimestamp(System.currentTimeMillis());
        
        rabbitTemplate.convertAndSend("seckill.exchange", 
                                       "seckill.order", 
                                       message);
        
        // 6. 立即返回(不等订单创建完成)
        return Result.success("秒杀请求已提交,正在处理中...");
    }
}

Step 2: 消息消费者(创建订单)

@Component
public class SeckillOrderConsumer {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private StockService stockService;
    
    /**
     * 消费秒杀消息,创建订单
     */
    @RabbitListener(queues = "seckill.order.queue")
    public void handleSeckillOrder(SeckillMessage message) {
        try {
            Long productId = message.getProductId();
            Long userId = message.getUserId();
            
            System.out.println("📮 收到秒杀消息:用户" + userId + " 购买商品" + productId);
            
            // 1. 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setUserId(userId);
            order.setOrderNo(generateOrderNo());
            order.setStatus(OrderStatus.UNPAID);
            order.setCreateTime(new Date());
            
            orderService.createOrder(order);
            
            // 2. 扣减数据库库存(最终一致性)
            boolean success = stockService.decrStock(productId, 1);
            if (!success) {
                // 如果数据库扣减失败,回滚Redis库存
                redisTemplate.opsForValue().increment("seckill:stock:" + productId);
                orderService.cancelOrder(order.getOrderNo());
                System.out.println("❌ 数据库库存不足,订单已取消");
                return;
            }
            
            // 3. 通知用户(通过WebSocket或短信)
            notifyUser(userId, "秒杀成功!请在15分钟内完成支付~");
            
            System.out.println("✅ 订单创建成功:" + order.getOrderNo());
            
        } catch (Exception e) {
            System.err.println("❌ 处理秒杀订单失败:" + e.getMessage());
            // 这里可以加入重试逻辑或死信队列
        }
    }
    
    /**
     * 生成订单号
     */
    private String generateOrderNo() {
        // 时间戳 + 随机数
        return "SK" + System.currentTimeMillis() + 
               (int)(Math.random() * 9000 + 1000);
    }
    
    /**
     * 通知用户
     */
    private void notifyUser(Long userId, String message) {
        // 实现WebSocket推送或短信通知
        // websocketService.send(userId, message);
    }
}

生活例子

消息队列就像餐厅的点餐系统:

  1. 你去餐厅点餐(发起秒杀)
  2. 服务员给你一个号码牌:"您的号码是88号,请等候"(立即返回)
  3. 后厨按顺序做菜(异步处理订单)
  4. 做好了叫号(推送结果)

而不是:

  1. 你去餐厅点餐
  2. 站在后厨门口等着厨师做完菜 ❌(同步等待)
  3. 拿到菜才走

4.6 第五招:数据库防超卖 🔐

虽然Redis已经控制了,但数据库也要防范!

4.6.1 乐观锁(版本号)

/**
 * 使用版本号实现乐观锁
 */
@Mapper
public interface StockMapper {
    
    /**
     * 扣减库存(乐观锁)
     * 
     * SQL: UPDATE product_stock 
     *      SET stock = stock - #{quantity},
     *          version = version + 1
     *      WHERE product_id = #{productId}
     *        AND stock >= #{quantity}
     *        AND version = #{version}
     */
    int decrStockWithVersion(@Param("productId") Long productId,
                              @Param("quantity") Integer quantity,
                              @Param("version") Integer version);
}
@Service
public class StockService {
    
    @Autowired
    private StockMapper stockMapper;
    
    /**
     * 扣减库存(带重试)
     */
    public boolean decrStock(Long productId, Integer quantity) {
        int retryTimes = 3; // 最多重试3次
        
        for (int i = 0; i < retryTimes; i++) {
            // 查询当前库存和版本号
            ProductStock stock = stockMapper.selectByProductId(productId);
            
            if (stock.getStock() < quantity) {
                return false; // 库存不足
            }
            
            // 尝试扣减
            int affected = stockMapper.decrStockWithVersion(
                productId, 
                quantity, 
                stock.getVersion()
            );
            
            if (affected > 0) {
                System.out.println("✅ 数据库扣减成功");
                return true;
            }
            
            // 版本号冲突,重试
            System.out.println("⚠️ 版本冲突,重试第" + (i + 1) + "次");
        }
        
        return false;
    }
}

4.6.2 悲观锁(行锁)

/**
 * 使用行锁(悲观锁)
 */
@Mapper
public interface StockMapper {
    
    /**
     * 查询库存(加锁)
     * 
     * SQL: SELECT * FROM product_stock
     *      WHERE product_id = #{productId}
     *      FOR UPDATE
     */
    ProductStock selectByIdForUpdate(@Param("productId") Long productId);
    
    /**
     * 扣减库存
     */
    int decrStock(@Param("productId") Long productId,
                  @Param("quantity") Integer quantity);
}
@Service
public class StockService {
    
    @Autowired
    private StockMapper stockMapper;
    
    /**
     * 扣减库存(悲观锁)
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean decrStockPessimistic(Long productId, Integer quantity) {
        // 1. 查询库存并加锁(FOR UPDATE)
        ProductStock stock = stockMapper.selectByIdForUpdate(productId);
        
        // 2. 判断库存是否充足
        if (stock.getStock() < quantity) {
            return false;
        }
        
        // 3. 扣减库存
        int affected = stockMapper.decrStock(productId, quantity);
        
        return affected > 0;
    }
}

乐观锁 vs 悲观锁

特性乐观锁悲观锁
加锁时机更新时查询时
性能高(冲突少时)低(会阻塞)
适用场景冲突少冲突多
实现方式版本号FOR UPDATE

生活例子

  • 乐观锁:超市买东西,你拿了一瓶牛奶(version=1),到收银台发现标签变了(version=2),说明被人换了,你重新去拿
  • 悲观锁:你拿牛奶的时候直接抱住整个冰柜,不让别人碰,拿完才放开

秒杀场景推荐:用乐观锁!因为:

  • Redis已经控制了大部分流量
  • 到数据库的请求不多,冲突概率低
  • 乐观锁性能更好

4.7 第六招:限流算法 🚦

4.7.1 计数器算法(最简单)

/**
 * 简单计数器限流
 */
public class CounterRateLimiter {
    
    private final int maxCount;  // 时间窗口内最大请求数
    private final long windowSize; // 时间窗口大小(毫秒)
    
    private AtomicInteger count = new AtomicInteger(0);
    private long windowStart = System.currentTimeMillis();
    
    public CounterRateLimiter(int maxCount, long windowSize) {
        this.maxCount = maxCount;
        this.windowSize = windowSize;
    }
    
    /**
     * 尝试获取令牌
     */
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 如果超过时间窗口,重置计数器
        if (now - windowStart > windowSize) {
            count.set(0);
            windowStart = now;
        }
        
        // 判断是否超过限制
        if (count.get() < maxCount) {
            count.incrementAndGet();
            return true;
        }
        
        return false;
    }
}

缺点:临界点问题

时间窗口:[0s-1s],限制100个请求

情况:
0.9s:来了100个请求 ✅
1.0s:窗口重置
1.1s:又来了100个请求 ✅

结果:0.2秒内处理了200个请求!(限流失效)

4.7.2 滑动窗口算法(更精确)

/**
 * 滑动窗口限流
 */
public class SlidingWindowRateLimiter {
    
    private final int maxCount;
    private final long windowSize;
    
    // 使用队列记录每个请求的时间戳
    private Queue<Long> requestQueue = new LinkedList<>();
    
    public SlidingWindowRateLimiter(int maxCount, long windowSize) {
        this.maxCount = maxCount;
        this.windowSize = windowSize;
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 移除窗口外的请求
        while (!requestQueue.isEmpty() && 
               now - requestQueue.peek() > windowSize) {
            requestQueue.poll();
        }
        
        // 判断是否超过限制
        if (requestQueue.size() < maxCount) {
            requestQueue.offer(now);
            return true;
        }
        
        return false;
    }
}

4.7.3 令牌桶算法(推荐!)⭐

/**
 * 令牌桶限流(使用Guava)
 */
import com.google.common.util.concurrent.RateLimiter;

public class TokenBucketRateLimiter {
    
    // 每秒生成1000个令牌
    private RateLimiter rateLimiter = RateLimiter.create(1000);
    
    /**
     * 尝试获取令牌
     */
    public boolean tryAcquire() {
        // 尝试获取1个令牌,最多等待100ms
        return rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS);
    }
    
    /**
     * 阻塞获取令牌
     */
    public void acquire() {
        rateLimiter.acquire(1);
    }
}

使用示例

@RestController
public class SeckillController {
    
    // 每秒最多处理10000个请求
    private TokenBucketRateLimiter rateLimiter = 
        new TokenBucketRateLimiter(10000);
    
    @PostMapping("/seckill")
    public Result seckill(@RequestParam Long productId, 
                          @RequestParam Long userId) {
        
        // 尝试获取令牌
        if (!rateLimiter.tryAcquire()) {
            return Result.fail("系统繁忙,请稍后再试~");
        }
        
        // 执行秒杀逻辑
        return doSeckill(productId, userId);
    }
}

令牌桶算法原理

        [令牌桶] (容量100)
            │
            │ 每秒生成10个令牌
            ▼
        ┌───────┐
        │🪙🪙🪙│
        │🪙🪙🪙│
        │🪙🪙🪙│
        └───────┘
            ▲
            │ 每个请求消耗1个令牌
        [请求]

生活例子

令牌桶就像游乐园的游戏币:

  1. 🎰 售币机每秒产生10个游戏币(固定速率)
  2. 🧒 小朋友来玩游戏需要1个游戏币
  3. 🪙 有币就能玩,没币就等着
  4. 💰 币最多存100个,多了就丢弃

4.8 第七招:分布式锁 🔒

场景:多个服务器同时扣减库存,如何保证同一时刻只有一个能成功?

4.8.1 Redis分布式锁(基础版)

/**
 * Redis分布式锁(基础版)
 */
public class RedisLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 加锁
     * 
     * @param key 锁的key
     * @param value 锁的value(通常用UUID)
     * @param expireTime 过期时间(秒)
     * @return 是否加锁成功
     */
    public boolean lock(String key, String value, long expireTime) {
        // SET key value NX EX expireTime
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放锁(使用Lua脚本保证原子性)
     */
    public boolean unlock(String key, String value) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(
            redisScript,
            Collections.singletonList(key),
            value
        );
        
        return result != null && result == 1;
    }
}

使用示例

public boolean decrStockWithLock(Long productId) {
    String lockKey = "seckill:lock:" + productId;
    String lockValue = UUID.randomUUID().toString();
    
    try {
        // 1. 尝试加锁(最多等待3秒)
        boolean locked = redisLock.lock(lockKey, lockValue, 10);
        if (!locked) {
            return false;
        }
        
        // 2. 执行业务逻辑(扣减库存)
        String stockKey = "seckill:stock:" + productId;
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        
        if (stock < 0) {
            redisTemplate.opsForValue().increment(stockKey);
            return false;
        }
        
        return true;
        
    } finally {
        // 3. 释放锁
        redisLock.unlock(lockKey, lockValue);
    }
}

4.8.2 Redisson分布式锁(推荐!)⭐

/**
 * Redisson分布式锁(更强大)
 */
@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setPassword("your_password");
        
        return Redisson.create(config);
    }
}
@Service
public class SeckillService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 使用Redisson分布式锁
     */
    public boolean decrStockWithRedisson(Long productId) {
        String lockKey = "seckill:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁(最多等待3秒,锁10秒后自动释放)
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                return false;
            }
            
            // 执行业务逻辑
            return decrStock(productId);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            // 释放锁(只有持有锁的线程才能释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson的优势

  1. 看门狗机制:自动续期,防止业务未执行完锁就过期
  2. 可重入锁:同一个线程可以多次获取锁
  3. 公平锁:按FIFO顺序获取锁
  4. 红锁(RedLock):多Redis实例的分布式锁

生活例子

分布式锁就像公共厕所的门锁:

  • 🚪 有人在里面,门就锁上,外面的人进不来
  • 🔑 只有里面的人才能开门
  • ⏰ 如果里面的人昏倒了(程序崩溃),10分钟后自动开锁(超时释放)
  • 👥 多个人排队等(公平锁)

4.9 第八招:库存预热 🔥

问题:秒杀开始前,如何保证Redis的库存已经加载好了?

/**
 * 定时任务:秒杀前5分钟预热库存
 */
@Component
public class StockWarmUpTask {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 每天23:55:00执行(00:00:00开始秒杀)
     */
    @Scheduled(cron = "0 55 23 * * ?")
    public void warmUpStock() {
        System.out.println("🔥 开始预热库存...");
        
        // 1. 查询所有秒杀商品
        List<SeckillProduct> products = productMapper.selectSeckillProducts();
        
        // 2. 加载到Redis
        for (SeckillProduct product : products) {
            String stockKey = "seckill:stock:" + product.getProductId();
            redisTemplate.opsForValue().set(stockKey, product.getStock());
            
            System.out.println("✅ 商品" + product.getProductId() + 
                               " 库存" + product.getStock() + " 已加载");
        }
        
        System.out.println("🎉 库存预热完成!");
    }
}

4.10 第九招:订单超时取消 ⏰

场景:用户秒杀成功但15分钟内没付款,需要释放库存

4.10.1 延迟队列方案

/**
 * 创建订单时发送延迟消息
 */
public void createOrder(Order order) {
    // 1. 保存订单
    orderMapper.insert(order);
    
    // 2. 发送延迟消息(15分钟后)
    rabbitTemplate.convertAndSend(
        "order.delay.exchange",
        "order.delay.key",
        order.getOrderNo(),
        message -> {
            // 设置消息延迟时间:15分钟 = 900000毫秒
            message.getMessageProperties().setDelay(15 * 60 * 1000);
            return message;
        }
    );
}
/**
 * 消费延迟消息,检查订单是否已支付
 */
@RabbitListener(queues = "order.timeout.queue")
public void handleOrderTimeout(String orderNo) {
    System.out.println("⏰ 检查订单超时:" + orderNo);
    
    // 1. 查询订单状态
    Order order = orderMapper.selectByOrderNo(orderNo);
    
    // 2. 如果未支付,取消订单
    if (order.getStatus() == OrderStatus.UNPAID) {
        System.out.println("❌ 订单超时未支付,自动取消");
        
        // 2.1 更新订单状态
        order.setStatus(OrderStatus.CANCELLED);
        orderMapper.updateById(order);
        
        // 2.2 回滚库存
        String stockKey = "seckill:stock:" + order.getProductId();
        redisTemplate.opsForValue().increment(stockKey);
        
        stockMapper.incrStock(order.getProductId(), 1);
        
        // 2.3 通知用户
        notifyUser(order.getUserId(), "订单已超时取消,库存已释放");
    } else {
        System.out.println("✅ 订单已支付,无需处理");
    }
}

4.10.2 Redis过期键监听(不推荐)

/**
 * 监听Redis key过期事件
 */
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    
    @Autowired
    private OrderService orderService;
    
    public RedisKeyExpirationListener(RedisMessageListenerContainer container) {
        super(container);
    }
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        
        // 格式:order:timeout:SK1634567890123
        if (expiredKey.startsWith("order:timeout:")) {
            String orderNo = expiredKey.replace("order:timeout:", "");
            orderService.cancelOrderIfUnpaid(orderNo);
        }
    }
}

为什么不推荐?

  • ⚠️ Redis key过期事件不保证实时性
  • ⚠️ 可能会丢失事件
  • ⚠️ 不适合生产环境

推荐方案对比

方案优点缺点
延迟队列可靠、精确需要MQ支持
定时任务扫描简单不够实时
Redis过期监听轻量不可靠

5. 代码实战 💻

5.1 完整的秒杀接口

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    @Autowired
    private TokenBucketRateLimiter rateLimiter;
    
    /**
     * 秒杀接口
     * 
     * 接口地址:POST /api/seckill/execute
     * 参数:
     *   - productId: 商品ID
     *   - userId: 用户ID
     *   - captcha: 验证码
     * 返回:
     *   - code: 200成功,其他失败
     *   - message: 提示信息
     *   - data: 订单号(成功时)
     */
    @PostMapping("/execute")
    public Result<String> seckill(
            @RequestParam Long productId,
            @RequestParam Long userId,
            @RequestParam String captcha,
            HttpServletRequest request) {
        
        // ============ 第一层:前置校验 ============
        
        // 1. 参数校验
        if (productId == null || userId == null) {
            return Result.fail("参数错误");
        }
        
        // 2. 验证码校验
        String sessionCaptcha = (String) request.getSession()
            .getAttribute("captcha");
        if (!captcha.equalsIgnoreCase(sessionCaptcha)) {
            return Result.fail("验证码错误");
        }
        
        // 3. 限流(令牌桶)
        if (!rateLimiter.tryAcquire()) {
            return Result.fail("系统繁忙,请稍后再试");
        }
        
        // ============ 第二层:业务校验 ============
        
        // 4. 检查秒杀活动是否开始/结束
        if (!seckillService.isActivityValid(productId)) {
            return Result.fail("秒杀活动未开始或已结束");
        }
        
        // 5. 检查用户是否已经参与过
        if (seckillService.hasUserParticipated(productId, userId)) {
            return Result.fail("您已经参加过秒杀,不能重复购买");
        }
        
        // ============ 第三层:执行秒杀 ============
        
        // 6. 执行秒杀逻辑
        String orderNo = seckillService.executeSeckill(productId, userId);
        
        if (orderNo != null) {
            return Result.success(orderNo, "秒杀成功,请在15分钟内完成支付");
        } else {
            return Result.fail("商品已售罄");
        }
    }
    
    /**
     * 查询秒杀结果
     */
    @GetMapping("/result")
    public Result<SeckillResult> getSeckillResult(
            @RequestParam Long productId,
            @RequestParam Long userId) {
        
        SeckillResult result = seckillService.getSeckillResult(productId, userId);
        return Result.success(result);
    }
}

5.2 核心Service实现

@Service
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 执行秒杀
     */
    public String executeSeckill(Long productId, Long userId) {
        
        // 1. 检查库存(Redis)
        String stockKey = "seckill:stock:" + productId;
        Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
        
        if (stock == null || stock <= 0) {
            return null;
        }
        
        // 2. 扣减库存(Lua脚本保证原子性)
        boolean decrSuccess = decrStockByLua(productId);
        if (!decrSuccess) {
            return null;
        }
        
        // 3. 标记用户已参与(防止重复购买)
        String userKey = "seckill:user:" + productId + ":" + userId;
        redisTemplate.opsForValue().set(userKey, "1", 24, TimeUnit.HOURS);
        
        // 4. 生成订单号
        String orderNo = generateOrderNo(userId);
        
        // 5. 发送MQ消息(异步创建订单)
        SeckillMessage message = new SeckillMessage();
        message.setProductId(productId);
        message.setUserId(userId);
        message.setOrderNo(orderNo);
        message.setTimestamp(System.currentTimeMillis());
        
        rabbitTemplate.convertAndSend(
            "seckill.exchange",
            "seckill.order",
            message
        );
        
        // 6. 返回订单号
        return orderNo;
    }
    
    /**
     * 使用Lua脚本扣减库存
     */
    private boolean decrStockByLua(Long productId) {
        String stockKey = "seckill:stock:" + productId;
        
        String luaScript = 
            "local stock = redis.call('get', KEYS[1]) " +
            "if tonumber(stock) > 0 then " +
            "    redis.call('decr', KEYS[1]) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(
            redisScript,
            Collections.singletonList(stockKey)
        );
        
        return result != null && result == 1;
    }
    
    /**
     * 生成订单号
     */
    private String generateOrderNo(Long userId) {
        // 格式:SK + 时间戳 + 用户ID后4位 + 随机4位
        String timestamp = String.valueOf(System.currentTimeMillis());
        String userSuffix = String.format("%04d", userId % 10000);
        String random = String.format("%04d", (int)(Math.random() * 10000));
        
        return "SK" + timestamp + userSuffix + random;
    }
    
    /**
     * 检查用户是否已参与
     */
    public boolean hasUserParticipated(Long productId, Long userId) {
        String userKey = "seckill:user:" + productId + ":" + userId;
        return Boolean.TRUE.equals(redisTemplate.hasKey(userKey));
    }
    
    /**
     * 检查活动是否有效
     */
    public boolean isActivityValid(Long productId) {
        String activityKey = "seckill:activity:" + productId;
        SeckillActivity activity = (SeckillActivity) redisTemplate
            .opsForValue().get(activityKey);
        
        if (activity == null) {
            return false;
        }
        
        long now = System.currentTimeMillis();
        return now >= activity.getStartTime() && now <= activity.getEndTime();
    }
}

5.3 数据库表设计

-- 商品库存表
CREATE TABLE product_stock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL COMMENT '商品ID',
    stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
    version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_product_id (product_id),
    INDEX idx_stock (stock)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品库存表';

-- 秒杀活动表
CREATE TABLE seckill_activity (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    activity_name VARCHAR(100) NOT NULL COMMENT '活动名称',
    product_id BIGINT NOT NULL COMMENT '商品ID',
    seckill_price DECIMAL(10,2) NOT NULL COMMENT '秒杀价格',
    stock INT NOT NULL COMMENT '秒杀库存',
    start_time DATETIME NOT NULL COMMENT '开始时间',
    end_time DATETIME NOT NULL COMMENT '结束时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_product_id (product_id),
    INDEX idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀活动表';

-- 订单表
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL COMMENT '订单号',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    product_id BIGINT NOT NULL COMMENT '商品ID',
    product_name VARCHAR(200) COMMENT '商品名称',
    price DECIMAL(10,2) NOT NULL COMMENT '价格',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待支付 1已支付 2已取消',
    pay_time DATETIME COMMENT '支付时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_order_no (order_no),
    INDEX idx_user_id (user_id),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

5.4 配置文件

# application.yml
server:
  port: 8080

spring:
  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    password: your_password
    database: 0
    lettuce:
      pool:
        max-active: 200
        max-idle: 20
        min-idle: 10
        max-wait: 1000ms
    timeout: 3000ms
  
  # RabbitMQ配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动确认
        concurrency: 5 # 最小消费者数量
        max-concurrency: 10 # 最大消费者数量
        prefetch: 1 # 每次预取1条消息
  
  # MySQL配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/seckill?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10
      connection-timeout: 30000

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  level:
    root: INFO
    com.example.seckill: DEBUG

6. 上线前的自检清单 ✅

6.1 性能测试

# 使用JMeter压测

# 场景1:10万并发用户,持续1分钟
线程数:100000
Ramp-Up时间:10秒
持续时间:60秒

# 目标:
- TPS(每秒事务数) > 10000
- 响应时间 < 500ms
- 错误率 < 0.1%

6.2 监控告警

/**
 * 监控指标
 */
public class MonitorMetrics {
    
    // 1. 系统指标
    - CPU使用率 < 80%
    - 内存使用率 < 80%
    - 磁盘IO < 80%
    
    // 2. 应用指标
    - QPS(每秒查询数)
    - TPS(每秒事务数)
    - 响应时间(P50, P95, P99)
    - 错误率
    
    // 3. 业务指标
    - 库存剩余
    - 订单数量
    - 支付成功率
    
    // 4. 依赖服务指标
    - Redis连接数
    - MySQL连接数
    - RabbitMQ消息堆积量
}

6.3 容错降级

/**
 * 降级策略
 */
@Service
public class SeckillFallbackService {
    
    /**
     * Redis挂了,降级到数据库
     */
    @HystrixCommand(fallbackMethod = "decrStockFromDB")
    public boolean decrStock(Long productId) {
        // 尝试从Redis扣减
        return redisService.decrStock(productId);
    }
    
    /**
     * 降级方法
     */
    public boolean decrStockFromDB(Long productId) {
        System.out.println("⚠️ Redis降级,使用数据库");
        return dbService.decrStock(productId);
    }
    
    /**
     * MQ挂了,降级到同步创建订单
     */
    public String createOrder(Long productId, Long userId) {
        try {
            // 尝试发送MQ消息
            mqService.sendMessage(productId, userId);
        } catch (Exception e) {
            // MQ挂了,同步创建订单
            System.out.println("⚠️ MQ降级,同步创建订单");
            return orderService.createOrderSync(productId, userId);
        }
    }
}

6.4 回滚方案

/**
 * 紧急回滚方案
 */
public class EmergencyRollback {
    
    /**
     * 方案1:关闭秒杀入口
     */
    public void closeEntry() {
        // 修改配置中心的开关
        configService.set("seckill.enabled", "false");
        
        // 前端显示:活动已结束
    }
    
    /**
     * 方案2:限制流量
     */
    public void limitTraffic() {
        // 把限流阈值降到最低
        rateLimiter.setRate(100); // 每秒只允许100个请求
    }
    
    /**
     * 方案3:回滚代码
     */
    public void rollbackCode() {
        // 使用蓝绿部署,切换到上一个稳定版本
        // 或者使用Git回滚代码重新部署
    }
}

7. 总结 🎓

7.1 架构总览

┌─────────────────────────────────────────────────────────┐
│                     秒杀系统全景图                        │
└─────────────────────────────────────────────────────────┘

前端层:
├─ 验证码(防刷)
├─ 按钮置灰(防重复点击)
└─ 页面静态化(CDN加速)

网关层:
├─ Nginx负载均衡
├─ 限流(每秒1万请求)
└─ 黑名单(IP/用户)

应用层:
├─ 令牌桶限流
├─ 参数校验
├─ 业务校验
└─ 执行秒杀逻辑

缓存层:
├─ Redis集群(主从 + 哨兵)
├─ 库存预热
└─ Lua脚本(原子扣减)

消息层:
├─ RabbitMQ/Kafka
├─ 异步创建订单
└─ 延迟队列(超时取消)

数据层:
├─ MySQL主从(读写分离)
├─ 乐观锁(防超卖)
└─ 分布式事务(最终一致性)

7.2 核心要点

层级技术方案作用
前端验证码 + 按钮置灰拦截99%无效请求
网关Nginx限流控制进入后端的流量
应用令牌桶 + 参数校验二次限流 + 防非法请求
缓存Redis + Lua脚本高性能扣库存 + 防超卖
队列RabbitMQ异步处理 + 削峰填谷
数据库乐观锁 + 事务最后一道防线

7.3 性能对比

优化前 vs 优化后

┌─────────────┬────────────┬────────────┐
│    指标     │  优化前    │  优化后    │
├─────────────┼────────────┼────────────┤
│ QPS         │ 500        │ 50000      │
│ 响应时间    │ 5000ms     │ 100ms      │
│ 超卖概率    │ 高         │ 0          │
│ 系统稳定性  │ 经常崩溃   │ 稳如老狗   │
│ 用户体验    │ 😭         │ 😄         │
└─────────────┴────────────┴────────────┘

7.4 老程序员的忠告 💡

  1. 稳定压倒一切:宁可慢一点,也不能崩!崩了就是0!
  2. 不要相信任何人:前端可以伪造、网络可能延迟、数据库可能挂
  3. 多层防护:前端限流 → 网关限流 → 应用限流 → 缓存限流 → 数据库限流
  4. 异步能用就用:能异步的绝不同步,MQ是你的好朋友
  5. 监控和告警:没有监控的系统就是裸奔,出了问题都不知道
  6. 演练和预案:上线前必须压测,出问题要有回滚方案

7.5 面试加分项 ⭐

如果你能在面试时说出这些,基本上offer就稳了:

  1. 说出整体架构:前端 → 网关 → 应用 → 缓存 → 队列 → 数据库
  2. 说出限流方案:令牌桶算法 + 代码实现
  3. 说出防超卖方案:Redis Lua脚本 + 数据库乐观锁
  4. 说出削峰方案:消息队列异步处理
  5. 说出容灾方案:降级、熔断、回滚
  6. 说出监控方案:系统指标 + 业务指标 + 告警

8. 彩蛋:压测脚本 🎁

8.1 JMeter压测

<!-- 下载JMeter: https://jmeter.apache.org/ -->

<!-- 压测计划 -->
<TestPlan>
    <ThreadGroup>
        <name>秒杀压测</name>
        <num_threads>10000</num_threads>  <!-- 1万并发 -->
        <ramp_time>10</ramp_time>  <!-- 10秒内启动 -->
        <duration>60</duration>  <!-- 持续60秒 -->
    </ThreadGroup>
    
    <HTTPSamplerProxy>
        <domain>localhost</domain>
        <port>8080</port>
        <path>/api/seckill/execute</path>
        <method>POST</method>
        <arguments>
            <HTTPArgument>
                <name>productId</name>
                <value>1001</value>
            </HTTPArgument>
            <HTTPArgument>
                <name>userId</name>
                <value>${__Random(1,1000000)}</value>
            </HTTPArgument>
            <HTTPArgument>
                <name>captcha</name>
                <value>1234</value>
            </HTTPArgument>
        </arguments>
    </HTTPSamplerProxy>
</TestPlan>

8.2 压测命令

# 启动JMeter(GUI模式)
./jmeter.sh

# 命令行模式(推荐)
./jmeter.sh -n -t seckill_test.jmx -l result.jtl -e -o report/

# 参数说明:
# -n: 命令行模式
# -t: 测试计划文件
# -l: 结果文件
# -e: 生成报告
# -o: 报告目录

🎉 写在最后

老哥写这篇文档整整花了3天,喝了10杯咖啡 ☕☕☕,薅掉了不少头发 🧑‍🦲...

如果这篇文档帮到了你,记得:

  1. 给个Star(如果是在GitHub上的话)
  2. 📢 分享给朋友(独乐乐不如众乐乐)
  3. 💬 留个言(有问题欢迎骚扰)

记住:没有完美的架构,只有适合的架构!

最后送你一句话:

💪 代码写得好,工资少不了!

💰 系统不崩溃,年终奖翻倍!

祝你面试顺利,offer拿到手软!🎊


作者:10年老Java(真的有10年!)
微信:[你的微信]
GitHub:[你的GitHub]
邮箱:[你的邮箱]

声明:本文档仅供学习参考,实际项目请根据业务场景调整!


附录:参考资料 📚

  1. 阿里巴巴Java开发手册
  2. Redis官方文档
  3. RabbitMQ官方文档
  4. Spring Boot官方文档
  5. MyBatis-Plus官方文档
  6. 《高并发系统设计40问》- 极客时间
  7. 《亿级流量网站架构核心技术》- 张开涛
  8. 《Redis设计与实现》- 黄健宏

🔥🔥🔥 秒杀系统,秒的是速度,杀的是Bug!🔥🔥🔥

愿你的系统永不崩溃,愿你的代码永无Bug!

❤️ 2025年,我们继续加油!❤️