【面试情景剧】大厂面试官灵魂拷问电商秒杀系统扛不住谢飞机被问到「回家等通知」业务场景电商秒杀技术栈Spring Boot Redis Kafka MySQL Redisson分布式锁人物面试官严肃专业、谢飞机搞笑水货程序员 开场面试官端坐在会议室里面前摆着一杯已经凉了的美式咖啡。门被推开谢飞机穿着一件印有「BUG FREE」的T恤大步走进来胸前还别着一个工牌——上面写着「谢飞机 · 全栈全站在旁边看」。面试官面无表情请坐。看你简历上写做过电商项目今天我们就围绕秒杀系统来聊聊。谢飞机自信满满地坐下面试官您好秒杀我太熟了不就是「限时打折」嘛我双十一秒过三箱牛奶、两袋狗粮、还有一台……算了不说了。面试官……我说的是技术层面的秒杀系统。谢飞机哦哦技术啊那我也熟我天天被秒杀——被需求秒杀。 第一轮基础与业务衔接问题1请你描述一下秒杀系统的核心业务流程。谢飞机来了精神这个简单用户点「抢购」按钮 → 后端检查库存 → 库存够就扣减 → 生成订单 → 完事儿跟我去超市抢鸡蛋一个流程。面试官微微点头基本流程没问题。那你想想如果10万人同时抢1000台手机你这个流程能扛住吗谢飞机愣了一秒呃……扛不住的话就多叫几个超市……不是多开几台服务器面试官叹了口气方向是对的但远远不够。我们继续。问题2秒杀场景下库存扣减为什么不能直接操作MySQL谢飞机挠头因为……MySQL太慢了就像……你让一个老大爷去数10万个人的入场券他数到一半人就都冲进来了。面试官居然笑了比喻倒是挺形象。没错MySQL在高并发下会成为瓶颈。那你觉得应该用什么谢飞机抢答RedisRedis在内存里跑快就跟把老大爷换成高铁检票口一样面试官点头不错这个知识点答对了。Redis基于内存操作单机QPS可以达到10万级别是处理高并发库存扣减的首选。那我们继续深入。问题3用Redis扣减库存你怎么保证不超卖谢飞机自信这个我会用Redis的DECR命令啊它是原子操作一次只能减1不会超卖就像……就像自动售货机投一个币出一瓶水不会多给你。面试官挑眉DECR确实是原子操作基本思路是对的。那我问你如果你的秒杀系统是分布式部署的多台服务器同时执行DECR还能保证不超卖吗谢飞机笑容逐渐凝固呃……多台服务器……那它们不是都连着同一个Redis吗应该……还是可以的吧面试官严肃DECR本身是原子的多个客户端并发执行确实不会超卖这一点你说对了。但问题在于仅仅DECR不够——你还需要保证「检查库存」和「扣减库存」这两步操作的原子性。如果分开执行在检查和扣减之间库存可能已经被别人扣了。这就是典型的竞态条件问题。谢飞机恍然大悟状哦所以就像两个人同时看到最后一瓶水都伸手去拿……面试官对所以我们需要更严谨的方案。后面会考你。问题4秒杀请求到达后端之前前端可以做哪些优化谢飞机来劲了前端这个我知道按钮点一次就变灰不让重复点还有……还有……冥思苦想面试官还有呢谢飞机还有……让页面好看一点让用户忘了自己在抢东西面试官扶额前端优化远不止这些。常见的包括按钮防重复点击、倒计时前不展示抢购按钮、请求排队/验证码削峰、CDN静态化页面减少服务器压力、本地缓存秒杀令牌等。你回去整理一下。谢飞机疯狂记笔记好的好的面试官说得对…… 第二轮进阶与原理探究问题5你说用Redis预扣库存那Redis和MySQL之间的数据一致性怎么保证谢飞机开始含糊这个……就是……Redis扣完了然后……然后写个消息……不对写个定时任务……把数据同步到MySQL就像……就像你在备忘录上记了花了多少钱晚上回家再记到账本上。面试官追问那如果Redis扣减成功了但消息发送失败或者同步到MySQL的时候失败了怎么办谢飞机眼神开始飘忽那就……重试或者……人工对账实在不行……让运维兄弟手动修一下数据面试官严肃这在生产环境是不可接受的。正确的做法是Redis预扣库存成功后发送消息到Kafka由消费者异步创建订单并扣减MySQL库存。如果消息发送失败需要回滚Redis库存。同时消费者端要做好幂等性处理防止重复消费。这就是经典的最终一致性方案。谢飞机小声最终一致性……就是「最终会一致但中间可能不一致」的意思吧面试官理解没错但实现起来要严谨得多。问题6Kafka在这个场景中起什么作用为什么选Kafka而不是RabbitMQ谢飞机试图蒙混过关Kafka嘛……就是……一个消息队列用来……异步的削峰的至于为什么选Kafka不选RabbitMQ……因为Kafka名字短面试官面无表情名字短谢飞机赶紧找补不是不是因为Kafka……它……吞吐量高我看网上都这么说的面试官吞吐量高是对的但不够全面。秒杀场景选择Kafka的核心原因有三点超高吞吐量Kafka单机写入TPS可达数十万适合秒杀瞬间的流量洪峰消息持久化Kafka将消息顺序写入磁盘配合零拷贝Zero-Copy技术持久化性能极高消费者组机制支持多个消费者组并行消费便于扩展。而RabbitMQ的优势在于路由灵活、消息可靠性更强、延迟更低适合对延迟敏感但吞吐量要求不那么极端的场景。谢飞机崇拜脸面试官您是不是背过标准答案面试官……这是基本功。问题7如果Kafka消费者创建订单时发现MySQL库存不足了怎么办谢飞机思考了一下那……就告诉用户抢失败了把Redis里扣的库存加回去面试官方向对但具体怎么做Redis库存已经扣了MySQL库存不够这个不一致怎么处理谢飞机开始冒汗呃……可以……发一条补偿消息或者……用那个……那个什么事务面试官严肃这里涉及两个关键点Redis预扣库存时要留buffer比如实际1000台Redis里只放1000个库存令牌不会超发消费者端幂等补偿如果MySQL扣减失败理论上不应该因为Redis已经做了第一层拦截需要发送补偿消息回滚Redis库存。核心思路是Redis做前置拦截MySQL做最终兜底。问题8分布式环境下如何防止同一个用户重复下单谢飞机抢答加锁用那个……synchronized面试官摇头synchronized只能在单JVM内生效分布式环境下无效。谢飞机慌了那……用数据库唯一索引同一个用户ID秒杀活动ID建唯一索引面试官终于露出赞许的表情这个答对了。数据库唯一索引是防重的最后一道防线。除此之外还可以在Redis层用SETNX做分布式锁用户点击抢购时先尝试获取锁获取成功才能继续。但要注意锁的超时时间和释放机制。谢飞机松口气还好我蒙对了一个…… 第三轮架构与极端场景问题9如果Redis主节点挂了库存扣减正在进行中怎么处理谢飞机彻底慌了Redis挂了那……重启面试官盯着他重启需要多久重启期间用户请求怎么办正在扣减的库存数据丢不丢谢飞机开始胡言乱语那个……Redis有……哨兵对哨兵模式哨兵会自动……切换然后……数据就……回来了面试官严肃Redis Sentinel可以实现故障自动转移但存在一个核心问题异步复制可能导致数据丢失。主节点扣减了库存但还没同步到从节点就挂了切换后新主节点的库存数据是不一致的。谢飞机小声那……用那个……RedLock面试官RedLock是分布式锁的方案不是解决数据同步的。对于库存数据可以考虑Redis AOF持久化配置always或everysec策略减少数据丢失窗口库存数据重建机制从MySQL恢复Redis库存数据降级方案Redis不可用时直接降级为MySQL 分布式锁方案牺牲性能保证正确性。问题10秒杀系统如何做限流如果流量超过系统承载能力怎么办谢飞机已经放弃治疗限流……就是……限制流量用那个……Nginx的limit_req面试官Nginx限流是一层但不够。完整的限流方案应该是多层级的。谢飞机试图挣扎我知道还有……Sentinel阿里的Sentinel可以……配置规则……面试官点头Sentinel确实是一个选择。完整的秒杀限流方案通常包括前端层验证码、按钮防抖、请求排队网关层Nginx限流令牌桶/漏桶、用户维度限流服务层Sentinel熔断限流、接口级别流控库存层Redis预置库存本身就是最天然的限流——库存没了请求自然被拒绝。谢飞机喃喃自语原来库存就是最好的限流器……问题11秒杀结束后大量未支付的订单占用库存怎么处理谢飞机已经神志不清让……让用户快点付款发短信催他面试官无语技术上怎么解决谢飞机开始胡说那个……设个……超时时间超时了就……取消订单然后把库存……还回去面试官思路是对的但实现细节呢怎么做到订单超时自动取消轮询数据库吗谢飞机彻底摆烂我……我用定时任务每分钟扫一次数据库面试官叹气定时任务扫表在订单量大时性能极差。常见方案有延迟队列使用Kafka延迟消息或RabbitMQ的TTL死信队列订单创建时发送一条30分钟的延迟消息到期后检查订单状态未支付则取消并回滚库存Redis过期监听利用Redis的Key过期事件通知但不可靠不推荐用于核心业务RocketMQ事务消息支持延迟投递更适合这种场景。问题12最后一个问题——如果让你从零设计一个支撑百万QPS的秒杀系统你会怎么设计整体架构谢飞机沉默了10秒……面试官我能说「我会先辞职」吗面试官终于忍不住笑了好吧这个问题确实有难度。简单说一下思路百万QPS的秒杀系统核心架构思路是**「流量漏斗」**用户请求百万级 ↓ CDN 前端静态化拦截90% ↓ 网关层限流 用户鉴权拦截8% ↓ Redis预扣库存拦截1.9% ↓ Kafka异步下单仅成功请求进入 ↓ MySQL落库最终一致性每一层尽可能拦截流量让真正到达数据库的请求降到最低。同时配合服务降级、熔断、灰度发布等容灾手段。谢飞机目瞪口呆面试官您这不是面试这是上课啊……面试官收起笑容合上笔记本好的今天的面试就到这里。你的基础知识还有一些印象但在深度和系统性上还有很大提升空间。谢飞机紧张那……我……面试官回去等通知吧。谢飞机站起来小声嘀咕等通知……等通知……我上辈子等了多少通知了…… 文末技术解析小白也能看懂的秒杀系统全攻略下面由「技术主笔」为大家详细解析面试中涉及的所有技术点和业务场景确保小白读者也能学到干货一、秒杀系统核心业务流程┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │ 用户点击抢购 │ → │ 前端校验限流 │ → │ Redis预扣库存 │ → │ 发送MQ消息 │ └─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ ↓ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │ 返回抢购结果 │ ← │ 更新Redis状态 │ ← │ MySQL创建订单 │ ← │ 消费者异步处理 │ └─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘关键点秒杀的本质是**「在极高并发下保证数据一致性」**核心矛盾瞬时高并发vs库存数据的准确性解决思路层层拦截流量让真正到达数据库的请求尽可能少二、Redis预扣库存方案详解2.1 为什么不用MySQL直接扣// ❌ 错误方案直接操作MySQL // 高并发下大量请求打到MySQL导致数据库连接池耗尽、响应超时 Transactional public void deductStock(Long goodsId) { // UPDATE stock SET count count - 1 WHERE goods_id #{goodsId} AND count 0 // 问题行锁竞争激烈10万并发 10万个事务竞争同一行 }2.2 正确的Redis预扣库存方案// ✅ 正确方案Redis Lua脚本保证原子性 // 将「检查库存」和「扣减库存」合并为一个原子操作 String luaScript local stock tonumber(redis.call(get, KEYS[1])) if (stock 0) then return -1 end // 库存不足 if (redis.call(sismember, KEYS[2], ARGV[1]) 1) then return -2 end // 重复抢购 redis.call(decr, KEYS[1]) redis.call(sadd, KEYS[2], ARGV[1]) return 1; // KEYS[1] 库存key, KEYS[2] 已购用户集合 // ARGV[1] 用户ID Long result redisTemplate.execute(luaScript, keys, userId); if (result 1) { // 扣减成功发送MQ消息 kafkaTemplate.send(seckill-order-topic, orderMessage); } else if (result -1) { throw new BusinessException(库存不足); } else if (result -2) { throw new BusinessException(请勿重复抢购); }为什么用Lua脚本Redis执行Lua脚本是单线程原子操作不会被其他命令插入避免了「先GET检查 → 再DECR扣减」之间的竞态条件一次网络往返完成所有操作减少RTT延迟三、Kafka异步削峰详解3.1 为什么选Kafka| 特性 | Kafka | RabbitMQ | |------|-------|----------| | 单机吞吐量 | 数十万/秒 | 数万/秒 | | 消息持久化 | 顺序写磁盘零拷贝 | 内存磁盘 | | 延迟 | ms级别 | us级别 | | 适用场景 | 高吞吐、大数据量 | 低延迟、复杂路由 |秒杀场景的核心需求是超高吞吐量Kafka是更合适的选择。3.2 生产者代码Service public class SeckillProducer { Autowired private KafkaTemplateString, String kafkaTemplate; public void sendSeckillMessage(SeckillOrderMessage message) { // 以用户ID作为key保证同一用户的请求进入同一分区顺序消费 kafkaTemplate.send( seckill-order-topic, message.getUserId(), // partition key JSON.toJSONString(message) ).addCallback( result - log.info(消息发送成功: {}, message.getUserId()), ex - { log.error(消息发送失败: {}, ex.getMessage()); // 发送失败需要回滚Redis库存 rollbackRedisStock(message.getGoodsId()); } ); } }3.3 消费者代码含幂等处理Component public class SeckillOrderConsumer { KafkaListener(topics seckill-order-topic, groupId seckill-group) public void consume(String message) { SeckillOrderMessage orderMsg JSON.parseObject(message, SeckillOrderMessage.class); // 幂等性检查防止重复消费 String idempotentKey seckill:idempotent: orderMsg.getUserId() : orderMsg.getGoodsId(); Boolean acquired redisTemplate.opsForValue().setIfAbsent(idempotentKey, 1, 24, TimeUnit.HOURS); if (Boolean.FALSE.equals(acquired)) { log.warn(重复消息跳过: {}, orderMsg.getUserId()); return; } try { // 创建订单 扣减MySQL库存 createOrderAndDeductStock(orderMsg); } catch (Exception e) { // 失败则删除幂等key允许重试 redisTemplate.delete(idempotentKey); throw e; // 抛异常触发Kafka重试 } } }四、分布式锁防重复下单// 使用Redisson分布式锁 RLock lock redissonClient.getLock(seckill:lock: userId : goodsId); try { // 尝试获取锁最多等待1秒锁持有时间最长10秒 boolean acquired lock.tryLock(1, 10, TimeUnit.SECONDS); if (!acquired) { throw new BusinessException(系统繁忙请稍后重试); } // 执行业务逻辑 processSeckill(userId, goodsId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }关键点tryLock的等待时间不能太长秒杀场景下用户等不了锁的超时时间要合理太短会导致业务没执行完锁就释放了Redisson的看门狗机制会自动续期防止业务未执行完锁过期五、订单超时自动取消方案// 方案一Kafka延迟消息推荐 // 发送订单时同时发送一条30分钟的延迟消息 public void sendDelayCancelMessage(Long orderId) { // Kafka本身不直接支持延迟消息可通过以下方式实现 // 1. 使用独立的延迟Topic 定时扫描 // 2. 使用RocketMQ的延迟消息功能 // 3. 使用Redis的Key过期监听 // 这里以Redis 监听为例 String key seckill:order:timeout: orderId; redisTemplate.opsForValue().set(key, 1, 30, TimeUnit.MINUTES); } // 监听Redis Key过期事件 Component public class OrderTimeoutListener extends KeyExpirationEventMessageListener { public OrderTimeoutListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } Override public void onMessage(Message message, byte[] pattern) { String key message.toString(); if (key.startsWith(seckill:order:timeout:)) { Long orderId Long.parseLong(key.split(:)[3]); cancelTimeoutOrder(orderId); } } private void cancelTimeoutOrder(Long orderId) { // 检查订单状态未支付则取消并回滚库存 Order order orderMapper.selectById(orderId); if (order ! null order.getStatus() OrderStatus.UNPAID) { order.setStatus(OrderStatus.CANCELLED); orderMapper.updateById(order); // 回滚Redis库存 redisTemplate.opsForValue().increment(seckill:stock: order.getGoodsId()); } } }六、百万QPS架构设计总结┌─────────────────────────────────────┐ │ 用户请求百万QPS │ └──────────────┬──────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 第1层CDN 前端静态化 │ │ · 秒杀页面静态化CDN分发 │ │ · 按钮防抖 验证码 │ │ · 拦截约 90% 请求 │ └──────────────┬──────────────────────┘ ↓ ~10万QPS ┌─────────────────────────────────────┐ │ 第2层API网关限流 │ │ · Nginx limit_req 令牌桶 │ │ · 用户维度限流单用户每秒1次 │ │ · 黑名单过滤 │ │ · 拦截约 80% 剩余请求 │ └──────────────┬──────────────────────┘ ↓ ~2万QPS ┌─────────────────────────────────────┐ │ 第3层Redis预扣库存 │ │ · Lua脚本原子操作 │ │ · 库存扣完直接拒绝 │ │ · 拦截约 95% 剩余请求 │ └──────────────┬──────────────────────┘ ↓ ~1000 QPS ┌─────────────────────────────────────┐ │ 第4层Kafka异步下单 │ │ · 消息队列削峰填谷 │ │ · 消费者按能力消费 │ └──────────────┬──────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 第5层MySQL落库 │ │ · 最终一致性 │ │ · 唯一索引防重 │ └─────────────────────────────────────┘七、核心知识点速记卡| 知识点 | 核心要点 | |--------|----------| | Redis扣库存 | Lua脚本保证原子性避免竞态条件 | | Kafka削峰 | 异步解耦吞吐量数十万/秒 | | 分布式锁 | Redisson 看门狗自动续期 | | 幂等性 | Redis SETNX 唯一索引双重保障 | | 数据一致性 | Redis预扣 MQ异步 最终一致性 | | 限流方案 | 多层漏斗前端→网关→服务→库存 | | 订单超时 | 延迟队列 / Redis过期监听 | | 高可用 | Redis Sentinel/Cluster 降级方案 |谢飞机课后总结面试官说得对秒杀系统不是「限时打折」而是一场流量的战争。每一层拦截都是一道防线Redis是前线哨兵Kafka是后勤补给线MySQL是最后的大本营。至于我……我还是回去等通知吧。如果这篇文章对你有帮助欢迎点赞、收藏、关注下一期我们将带来「AIGC内容生成平台的Java架构设计」面试情景剧敬请期待