面试必杀技:如何把“秒杀系统”讲得明明白白?

在节假日的时候我们都喜欢去别的地方旅游,这个时候发现这想象一下12306放票瞬间,我们刚打开app准备抢票结果没了、或者双11的时候商品倒计时开抢还没点"抢"呢就按钮就灰了然后页面显示已经抢完。其实这背后都是"秒杀系统"在支撑。这个问题有时候面试的时候也会问到"秒杀"这个问题。

为什么会问到呢?因为这个问题它考察的东西还是挺多的,比如缓存,多线程,高并发等等。今儿个咱就分析分析这里面的道道儿。咱就以医院看病这种情况举例说明一下。

第一幕:挂号分诊(前端请求拦截)

病人一股脑儿挤进医院大门,肯定不行。秒杀开始前,系统就要开始"分诊":

  1. 按钮防暴击: 医院大门儿一开,病人全都往里面挤不停地挤,就相当于点击按钮一样狂点这样可不行,设置成5秒后才允许再次点击(防止用户狂点),挤一会儿也得歇一会儿吧!是不是有些类似。(不是很恰当,理解意思就行)
  2. 验证码拦截: 在提交订单前加入图形/滑块验证码,过滤掉无脑刷请求的脚本机器人,现在有的是让我们按顺序点击图片上出现的字儿,我感觉这个还是挺好玩儿,有时候看不准就点不对。
html 复制代码
<button id="seckillBtn" onclick="handleClick()">立即抢购</button>
<script>
  let canClick = true;
  function handleClick() {
    if (!canClick) return;
    canClick = false; // 立刻禁用按钮
    
    // 1. 模拟加入验证码校验 (这里简化)
    if (!validateCaptcha()) {
        canClick = true;
        alert('请完成验证码!');
        return;
    }
    
    // 2. 发起秒杀请求
    requestSeckill();
    
    // 3. 5秒后才允许再次点击 (实际场景根据后端响应动态开启更佳)
    setTimeout(() => { canClick = true; }, 5000);
  }
  function validateCaptcha() { return true; } // 模拟验证通过
  function requestSeckill() { /* 发送请求到后端 */ }
</script>

第二幕:诊室叫号(服务端核心逻辑)

挂号成功的人来到诊室门口,得有序叫号,不能一窝蜂冲进去把医生挤趴下。

  1. 请求排队: 用消息队列(如RabbitMQ, Kafka, RocketMQ)把瞬间的海量下单请求缓冲起来,后端服务根据自己的处理能力慢慢"叫号消费"。

  2. 库存关口: 这是秒杀的命门!绝对不能用数据库直接查减库存! 扛不住并发,还容易超卖(卖多了,朋友之前做电商发现有时候直接把库存干成负的了,哈哈)。必杀技:Redis 原子操作!

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;

public class SeckillService {

    private final JedisPool jedisPool;
    private static final String STOCK_PREFIX = "seckill:stock:";
    private static final String LOCK_PREFIX = "seckill:lock:";
    private static final int LOCK_EXPIRE = 5; // 锁过期时间(秒)

    public SeckillService(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    // 初始化库存(项目启动时执行)
    public void initStock(String goodsId, int stock) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.set(STOCK_PREFIX + goodsId, String.valueOf(stock));
        }
    }

    // 秒杀核心方法(原子操作版)
    public boolean trySeckill(String goodsId, String userId) {
        String stockKey = STOCK_PREFIX + goodsId;
        String lockKey = LOCK_PREFIX + goodsId;

        try (Jedis jedis = jedisPool.getResource()) {
            // 第一重校验:快速判断库存
            String remainStock = jedis.get(stockKey);
            if (remainStock == null || Integer.parseInt(remainStock) <= 0) {
                return false;
            }

            // 获取分布式锁(防止极端情况下的超卖)
            String lockToken = tryGetLock(jedis, lockKey);
            if (lockToken == null) return false; // 获取锁失败直接返回

            try {
                // 第二重校验:加锁后再次确认库存
                remainStock = jedis.get(stockKey);
                if (remainStock == null || Integer.parseInt(remainStock) <= 0) {
                    return false;
                }

                // 原子操作:减少库存并返回最新值
                Long newStock = jedis.decr(stockKey);
                
                if (newStock >= 0) {
                    // 扣减成功!生成订单记录(异步处理)
                    asyncCreateOrder(goodsId, userId);
                    return true;
                } else {
                    // 库存不足,回滚操作
                    jedis.incr(stockKey);
                    return false;
                }
            } finally {
                // 释放分布式锁
                releaseLock(jedis, lockKey, lockToken);
            }
        }
    }

    // 获取分布式锁(简化版)
    private String tryGetLock(Jedis jedis, String key) {
        String token = String.valueOf(System.currentTimeMillis());
        String result = jedis.set(key, token, "NX", "EX", LOCK_EXPIRE);
        return "OK".equals(result) ? token : null;
    }

    // 释放分布式锁
    private void releaseLock(Jedis jedis, String key, String token) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                           "return redis.call('del', KEYS[1]) " +
                           "else return 0 end";
        jedis.eval(luaScript, Collections.singletonList(key), 
                   Collections.singletonList(token));
    }

    // 异步创建订单(实际开发中放入消息队列)
    private void asyncCreateOrder(String goodsId, String userId) {
        // 这里模拟异步处理
        new Thread(() -> {
            System.out.println("生成订单:商品=" + goodsId + " 用户=" + userId);
            // 实际业务:订单入库、扣减数据库库存等
        }).start();
    }
}

为什么用Lua脚本? 因为Redis单线程执行Lua脚本是原子的!GETDECR操作之间不会被其他命令打断,彻底杜绝超卖。这是秒杀系统的黄金搭档。

第三幕:药房配药 ------ 订单异步落库

医生开了处方,病人拿着单子去药房排队取药。不能堵在诊室门口等。

  1. 快速响应: 用户只要知道"我抢没抢到"这个结果要快(秒级)。至于生成完整订单、扣积分、通知发货...这些都是"药房"后台慢慢处理的"体力活"。
  2. 异步下单: 把生成订单明细、扣减数据库库存(最终一致性)、记录日志等耗时操作,放到消息队列里,由专门的订单处理服务慢慢消费完成。用户秒级知道"抢购成功",后面可能等几分钟才在"我的订单"里看到。

总结:秒杀系统

  1. 削峰填谷: 前端限流(挂号分诊) + 消息队列(排队叫号),把瞬时巨峰流量变成后端能承受的平缓河流。
  2. 缓存为王: 核心库存判断必须用Redis(诊室叫号),利用其超高并发能力和原子操作(Lua脚本),死守库存防线。
  3. 异步落地: 抢购结果快速返回,订单生成等耗时操作异步处理(药房配药),保证核心流程速度。

面试点睛之笔: 当被问到"怎么防止超卖?"时,目光坚定,脱口而出:"用Redis的原子操作在缓存层扣减和检查库存,数据库层最终异步扣减保证最终一致性。" 这句话,价值千金!


相关推荐
熊猫片沃子几秒前
Mybatis中进行批量修改的方法
java·后端·mybatis
养鱼的程序员3 分钟前
零基础搭建个人网站:从 Astro 框架到 GitHub 自动部署完全指南
前端·后端·github
白应穷奇12 分钟前
编写高性能数据处理代码 01
后端·python
拉不动的猪19 分钟前
jS篇Async await实现同步效果的原理
前端·javascript·面试
杨充23 分钟前
03.接口vs抽象类比较
前端·后端
一只叫煤球的猫24 分钟前
基于Redisson的高性能延迟队列架构设计与实现
java·redis·后端
卡尓26 分钟前
使用 Layui 替换 Yii 基础模板的默认 Bootstrap 样式并尝试重写导航栏组件
后端
WhyWhatHow30 分钟前
JEnv:新一代Java环境管理器,让多版本Java管理变得简单高效
java·后端
Rust语言中文社区1 小时前
Rust 训练营二期来袭: Rust + AI 智能硬件
开发语言·后端·rust
喵手1 小时前
如何实现一个简单的基于Spring Boot的用户权限管理系统?
java·spring boot·后端