上周有个读者私信我,说自己八股文背了三个月,MySQL索引、JVM垃圾回收、Redis持久化这些张口就来,结果面某头部电商平台时,面试官一句"那你讲讲秒杀场景下怎么保证库存不超卖",他直接卡了壳。这我太有感触了。前两年我自己面大厂Java岗位,也经历过一场一个半小时的连环拷问——从电商秒杀的业务细节一路追到微服务架构的底层原理,全程没有一道题是"背过就能答"的。那场面试之后我复盘了很久,发现大厂面试官根本不关心你会背多少知识点,他们关心的是你在真实业务场景里能不能做出靠谱的技术决策。
这篇实录,我就把那次面试里被深度拷问的问题、我当时怎么答的、以及后来复盘认为更好的答法,完整拆出来讲。内容围绕电商秒杀和微服务架构两条主线展开,涵盖了缓存、限流、库存扣减、分布式事务、服务拆分、注册中心选型这些高频考点。无论你是准备跳槽的Java开发,还是刚转行想进大厂的新人,这篇文章都会比你自己刷题管用得多。
1. 电商秒杀:从缓存一致性到库存扣减的连环拷问
面试官开门见山,让我讲讲简历上那个秒杀项目。我一听就知道,这是要从业务场景切技术细节了。秒杀这个场景之所以被大厂面试官偏爱,是因为它一个点能串起缓存、并发、消息队列、分布式锁、数据库事务一大堆东西,随便抓住一个方向都能往深处挖。
1.1 预热题:缓存穿透、击穿、雪崩,你真的分清楚了吗
第一个问题还算友好:"秒杀开始后,大量请求打到Redis上,你会怎么设计缓存?"我答了"先把商品详情缓存到Redis,key是商品ID,value是序列化后的JSON"。面试官紧接着追问:"那如果用户疯狂请求一个根本不存在的商品ID呢?"
这就是经典的缓存穿透。每次请求都拿一个不存在的key去查Redis,查不到就会穿透到MySQL,如果攻击者用脚本伪造大量不存在的ID,数据库会被直接打挂。我当时答了两种方案:一是缓存空值,查不到数据时也往Redis里写一个空值并设置较短的过期时间,比如60秒,这样后续相同请求直接命中空缓存,不会再打到DB;二是用布隆过滤器,把所有合法的商品ID提前初始化到Bloom Filter里,请求来了先判断ID是否存在,不存在直接返回"商品不存在"。
面试官点了点头,又问:"那如果这个商品是真的热点,缓存正好在秒杀开始那一瞬间过期了怎么办?"这是缓存击穿。热点key过期瞬间,大量并发请求同时回源到数据库,和穿透的区别在于:穿透是key压根不存在,击穿是key存在但缓存刚过期。我说可以用互斥锁,只让一个线程去查DB并重建缓存,其他线程等待;或者用逻辑过期,缓存里不存物理过期时间,而是存一个逻辑过期时间戳,发现逻辑过期后,当前线程先返回旧数据,然后后台异步线程去刷新缓存。
最后他补了一个"很多人会混淆"的问题:"如果大量key同时过期,或者Redis整个挂了,又该怎么办?"这是雪崩。我当时答的是:key的过期时间加随机值打散,避免同一时刻集中失效;Redis做高可用,主从+哨兵或者Cluster模式;再往上一层,接口做本地缓存(比如Caffeine)兜底,哪怕Redis全挂了,本地缓存还能扛一阵子;最后还有熔断降级,DB实在扛不住的时候直接返回"系统繁忙"。
这一轮下来,我发现面试官并不是要我背诵三个概念的定义,而是在验证我有没有真正处理过大流量的经验。三个问题的共性其实是同一件事:你怎么保障缓存系统的稳定性和最终一致性。穿透是防恶意请求,击穿是防热点失效,雪崩是防整体性故障,三者的防护手段有重叠但侧重点完全不同。
1.2 库存扣减:从超卖问题到原子操作的设计取舍
热身结束后,问题进入了核心地带:"秒杀的本质是抢库存,你怎么保证不会超卖?"
我心想:这题总算问到正点子上了。超卖的本质是多个并发请求同时扣减同一个库存,最后扣成了负数。我给出了三种方案,从常规到进阶逐一分析。
第一种,数据库乐观锁。SQL写成这样:
UPDATE t_stock SET stock = stock - 1 WHERE product_id = #{productId} AND stock > 0;执行后判断受影响行数,等于1说明扣减成功,等于0说明库存不足。这里的核心是stock > 0这个条件,它把"库存充足才能扣减"作为原子性约束交给数据库去保证。这种方案最实在,压测下来可以做到不超卖,但问题也很明显:秒杀期间所有请求都打到数据库行锁上,吞吐量上不去。秒杀玩的就是高并发,纯靠数据库撑不住。
第二种,Redis预扣减。秒杀开始前先把库存加载到Redis,扣减用Lua脚本保证原子性:
-- KEYS[1]: 用户去重集合 KEYS[2]: 库存key ARGV[1]: 用户ID if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then return -1 -- 用户已秒杀过 end local stock = tonumber(redis.call('GET', KEYS[2])) if stock == nil or stock <= 0 then return 0 -- 库存不足 end redis.call('SADD', KEYS[1], ARGV[1]) redis.call('DECR', KEYS[2]) return 1 -- 扣减成功这个脚本把"用户去重、库存判断、库存扣减、记录用户"四件事合成一个原子操作,在Redis单线程模型下天然不会产生竞态。我当时特意跟面试官强调:为什么必须用Lua脚本?因为如果拆成多条Redis命令,先GET再DECR,中间隔着一个网络往返,另一个请求可能已经把库存扣完了,你拿到的stock是过期数据,就会超卖。
第三种,异步落库。Redis扣减成功以后,不能直接返回"秒杀成功"就完事,数据库里的库存也得扣,否则重启之后库存就对不上了。我的做法是:Redis扣减成功就把订单消息发到MQ,消费端异步执行数据库库存更新,用之前那条stock > 0的UPDATE兜底,保证最终一致性。
面试官这时候笑了笑,追了一刀:"Redis里库存扣了,MQ消息还没来得及消费,数据库还没扣,这时候如果服务重启了,两边不一致怎么办?"这问的是Redis和DB的一致性问题。我的回答是:消息队列本身要保证不丢消息,生产者要开确认模式,消费者要手动ACK,消息持久化到磁盘;即便服务重启,消息还能从MQ里恢复消费,最终数据库一定会扣到。同时要建立对账机制,定期比对Redis剩余库存和数据库剩余库存,发现不一致用数据库为准做补偿。
这一小节走下来,我的体会是:库存扣减方案没有一个银弹,全是取舍。Redis快但丢数据风险高,数据库可靠但扛不住流量,所以工业界普遍做法是分级——用Redis挡住99%的流量,用MQ削峰,用数据库做最终裁决,每层各司其职。
1.3 削峰填谷:消息队列、限流与幂等性的组合拳
面试官没给我喘息的机会,直接问:"秒杀开始瞬间,可能有几十万请求涌入,你光有库存扣减还不行,服务怎么扛住?"
这就聊到了削峰填谷。我的方案分三层来拆。
第一层是网关限流。用Sentinel对秒杀接口配置流控规则,比如单机QPS限制在5000,超过的请求直接返回"排队中"。这里我特意提到了限流算法的选择:固定窗口计数器有临界问题——比如限制每分钟1000个请求,用户在59分59秒发了1000个,下一秒又发了1000个,实际上两秒内通过了2000个请求;滑动窗口把时间分成更细的格子,比如每秒一个槽位,统计最近60秒的请求总数,能缓解这个问题;令牌桶算法则更平滑,按固定速率往桶里放令牌,请求来了取令牌,桶满就拒绝,允许一定的突发流量。Guava的RateLimiter就是令牌桶的典型实现,Sentinel也内置了多种流控模式。
第二层是MQ异步化。秒杀下单没必要同步等数据库响应。我的做法是:接口收到请求后,做基础校验、Redis预扣减、生成订单号,然后把"创建订单"的消息丢进RocketMQ,接口立即返回"提交成功",用户看到的"下单中"状态。真正的订单创建、库存落库、优惠券核销全部由MQ消费者异步处理。这样秒杀接口的RT能从几百毫秒降到几十毫秒,系统的瞬时压力被MQ缓冲掉了。
第三层是接口幂等。用户可能因为前端重试、网关重发,同一个请求被送到后端两次。如果不去重,同一个用户就可能抢到两份库存。我当时的方案有两个:一是前置去重,也就是Lua脚本里的用户去重集合,同一个用户在同一场秒杀里只能扣一次;二是订单号唯一约束,数据库订单表给用户ID+活动ID建唯一索引,重复插入直接报错。两道防线配合,基本能拦掉重复请求。
这一整轮下来,我明显感觉到面试官在引导我从"一个点"走向"一条线"——先有缓存兜底,再有原子扣减,最后用MQ和幂等把整个链路串起来。这才是真实的秒杀架构,不是背一道缓存题就能糊弄过去的。
2. 微服务架构:服务拆分、注册中心与分布式事务的层层深入
秒杀这关过了,面试官话锋一转:"我看你项目里用了微服务架构,那你说说,如果让你从零设计一个电商系统的微服务架构,你第一步会做什么?"
这个问题看着开放,其实藏了一个陷阱。很多候选人上来就吹技术选型,说要用Spring Cloud Alibaba、Nacos、Gateway,结果面试官问一句"你服务的边界在哪、数据库怎么拆分"就露馅了。
2.1 服务拆分的粒度怎么定:业务域、数据归属和团队结构缺一不可
我当时答的第一步是:先不要想技术,先想业务边界。电商系统往大了说有用户、商品、订单、库存、支付、营销几个核心域。每个域拆成一个独立微服务,这是最粗粒度的拆分标准,依据是"业务功能的单一职责"。
面试官追问:"商品域内部要不要再拆?"这问的是微服务粒度。我当时的观点是:服务拆分不是越细越好,拆太细了服务间通信成本会吃掉拆分收益。一个服务应该具备三个特征:独立的业务语义、独立的数据存储、独立的部署单元。如果一个模块满足这三条,拆出去才有意义;如果只是几个类有逻辑关联,硬拆成两个服务,只会让代码从方法调用变成远程调用,平白增加网络开销和排查难度。
数据归属这块特别容易栽跟头。我遇到过很多团队,服务拆了,数据库还共用一个——订单服务和库存服务都连着同一个MySQL实例,一张大表几十个服务都在操作。这等于没拆。微服务最重要的标志是"数据自治",每个服务只能访问自己的数据库,其他服务的数据只能通过API获取。如果两个服务需要频繁join查询,通常说明服务边界画错了。
我还提到了康威定律:系统架构会复制组织的沟通结构。如果一个模块需要三个团队协同修改,那它大概率要拆成多个服务;如果一个服务只有一个团队维护,边界就清晰得多。当时面试官明显对这个回答比较满意,因为很多人只会背"高内聚低耦合",说不出拆分背后的组织学依据。
2.2 注册中心与配置中心:Eureka、Zookeeper、Nacos怎么选
接下来面试官问了个实战问题:"你们项目注册中心用的什么?为什么不用其他的?"这就是典型的选型题,考察的不只是会用,还有对比分析能力。
我当时项目用的是Nacos,我从CAP理论角度做了对比:
| 组件 | CAP偏向 | 一致性协议 | 临时实例与持久实例 | 适用场景 |
|---|---|---|---|---|
| Eureka | AP | Peer复制,节点间互相注册 | 临时实例为主 | 老牌Spring Cloud生态,已进入维护模式 |
| Zookeeper | CP | ZAB,Leader选举 | 临时+持久节点 | 强一致性要求高,但不适合大规模服务发现 |
| Nacos | AP/CP可切换 | Distro(AP)+ Raft(CP) | 临时+持久实例,双模式支持 | 国内主流,注册配置一体化 |
| Consul | CP | Raft | 服务注册,健康检查丰富 | 基础设施完善但功能偏重 |
我当时解释得很直接:Eureka的AP模型保证服务可用,哪怕部分节点挂了,服务还能被找到,但可能出现一个服务实例被误判为下线的情况;Zookeeper的CP模型保证强一致,但Leader选举期间整个集群不可用,对服务发现场景来说,"短暂的不可用"比"偶尔的数据不一致"更不能接受。所以Zookeeper本质上不太适合做注册中心,它更适合做分布式协调,比如分布式锁、选主。
Nacos好在哪里?它同时支持AP和CP,临时实例走AP,非临时实例走CP,还能搞配置中心管理,一套组件解决注册和配置两件事。Spring Cloud Alibaba生态在国产公司里铺开之后,Nacos几乎成了标配。我当时还补了一句:选注册中心不光是看一致性,还要看运维成本。Eureka虽然老,但架不住大量存量项目在用;Nacos虽然新,但有控制台、有命名空间、有配置回滚,运维更舒服。
配置中心这块我也顺带说了。Nacos Config支持配置的集中管理和动态刷新,改配置不用重启服务。我当时的经验是:配置项一定要分环境隔离,dev、test、prod各建一个namespace;核心配置比如数据库连接池、线程池参数要允许运行时调整;配置变更要灰度发布,先在少量实例上验证没问题再全量推。
2.3 分布式事务:从2PC到Seata,什么时候该用它
微服务架构下一个绕不开的坑就是分布式事务。面试官问得很直接:"你秒杀下单这个链路,跨了订单服务、库存服务、优惠券服务,怎么保证事务一致性?"
我当时的回答分了三层。第一层是:能不用分布式事务就不用,尽量通过流程设计规避。比如秒杀的下单流程里,先把订单状态定义为"待支付",创建订单和扣减库存不要求在同一个事务里,用户可以接受"订单创建中"这个中间态。通过让每个步骤最终都达到一致状态,而不是强实时一致,很多问题就化解了。
第二层是:如果业务确实需要强一致性,再考虑分布式事务方案。经典的2PC(两阶段提交)性能太差,需要锁住所有参与方资源,不适合高并发场景;TCC(Try-Confirm-Cancel)性能好但业务侵入性强,每个操作要编写三套逻辑,实现成本很高。如果只是中低频业务需要强一致,Seata的AT模式是一个不错的选择。
我当时特意讲了Seata AT模式的工作原理:它是基于全局事务ID,业务SQL正常执行,框架自动生成before image和after image,也就是数据快照,执行完以后把快照保存到undolog表;如果后续某个分支失败,协调者通知回滚,Seata根据undolog反向解析,把数据恢复成before image的状态。这样业务方几乎不用改代码,就能获得分布式事务能力。它的代价是:每个分支事务的提交都需要和TC交互,有额外的网络开销,数据量大的时候undolog膨胀需要定期清理。
第三层是最终一致性方案。用本地消息表或者事务消息实现:业务操作和消息写入放在同一个本地事务里,然后有一个后台线程扫描消息表并发送到MQ,消费端保证幂等消费。我特别强调了一个经验:做最终一致性方案时,幂等是重中之重,消息可能会重投,消费端要基于唯一键去重。
面试官这时候抛了一个很刁钻的角度:"我要是你的话,会在秒杀场景直接放弃分布式事务。"他解释说,秒杀链路要的是高吞吐和最终一致,如果每一步都卡着等事务提交,这个秒杀设计就失败了。真正的工业级做法是:Redis预扣减 + MQ异步下单 + 数据库乐观锁兜底 + 对账补偿。我用Seata AT模式,说明我知道分布式事务是什么、怎么用;但如果我一开始就主张在秒杀里引入Seata重型事务,反而会显得不懂业务。
这个反馈让我意识到:大厂面试官考的不只是你会不会用框架,更是你能不能判断在什么场景下不用框架。
2.4 全链路监控与故障排查:服务拆了,问题怎么定位
服务拆成十几个之后,一个请求会横跨多个服务。这时候最痛苦的不是写代码,而是排查问题。面试官问我:"用户报了一个下单失败,你怎么快速定位是哪个服务的问题?"
我答了三个工具组合。
第一个是全链路追踪。每个请求进来时生成一个全局TraceID,通过拦截器或过滤器注入到HTTP请求头里,服务间互相调用时把TraceID传递下去。日志框架里把TraceID也打进去,这样所有服务打印的日志都能通过同一个TraceID串起来。框架层面可以接Spring Cloud Sleuth + Zipkin,它们会自动生成Span和Trace,我们只需要在关键业务节点加一些自定义tag,比如商品ID、用户ID。
第二个是集中式日志。服务太多,不可能登录到每台机器上看日志。用ELK或者Loki做日志收集,通过TraceID去查询一条完整调用链路的所有日志。我的经验是:日志要打结构化的,最好用JSON格式,这样被采集之后,Kibana里能直接按字段筛选,比瞎看文本日志高效得多。
第三个是监控指标。用Prometheus + Grafana把每个服务的QPS、RT、错误率、JVM堆内存、GC次数接进来做成看板。我当时的做法是:每个服务都要暴露/actuator/prometheus端点,重点盯三个指标——接口P99响应时间、线程池活跃线程数、数据库连接池使用率。这三个指标一旦异常,基本能定位到系统瓶颈在哪一层。
我还分享了一个实际排查案例:某次线上订单大量失败,我先看监控看板发现订单服务的线程池活跃线程数飙到上限,说明不是下游挂,而是订单服务自己被拖住了;再看链路追踪,发现大量请求阻塞在访问商品服务上;最后翻Redis监控,发现商品缓存的命中率骤降,大量请求回源到数据库,数据库打满了。整个过程不到半小时,如果没有TraceID串联和监控看板,光靠人肉排查十几个服务,可能半天都找不到根因。
3. 面试官真正在考什么:我复盘出的三个评估维度
面完这个深度拷问环节,我最大的感受是:大厂面试题虽然看起来千变万化,但考核的逻辑是固定的。面试官从头到尾都在用三层漏斗筛人——技术能力、项目实战、思维方式。
3.1 技术深度:从"知道"到"理解"再到"决策"
一线开发往上走,面试官最反感的就是"背书式"回答。问你Redis持久化,你秒答"RDB和AOF"这说明你知道;问你什么时候用RDB什么时候用AOF,你能说出"RDB适合备份恢复、AOF适合防丢数据但文件大"这说明你理解;问你"AOF重写期间来了写请求怎么办",你能答出"重写完合并缓冲区里的增量数据"才算真正深入过源码。
面试官问"秒杀库存怎么不超卖",表面上在考乐观锁和Redis,实际上在考你的技术决策能力。如果你连"为什么先把库存放Redis再去锁数据库"这个前置条件都说不清,就说明没有真正做过高并发项目。我当时复盘得出一个方法:每学一个技术,都要问自己三个问题——它解决了什么场景的问题?它的代价是什么?什么情况下不该用它?这三问能筛掉八成"学了个寂寞"的人。
3.2 项目实战:剥洋葱式的"真实性验证"
大厂面试官问项目,通常有一套固定打法:先让你介绍,再选一个细节深挖,再换一个细节深挖,直到找到一个你答不上来的点为止。这不是故意刁难,而是在验证你简历上写的东西到底是不是自己做的。
我有个朋友踩过这个坑:简历上写了"使用Redis实现分布式锁解决库存超卖",面试官追问他分布式锁的key怎么设计、value用什么、过期时间设多少、如果业务执行超过过期时间怎么办,他全答不上来。后来他坦白说是"参考了网上的项目"才搭的,面试当场就结束了。
我的建议是把每一个技术点都做成"可追问三层":第一层是"我用它做了什么";第二层是"为什么这么做";第三层是"如果某些条件变了,方案哪里要调整"。一层比一层深入,相当于给自己准备了一个"项目答辩稿"。
3.3 算法题:不慌,先聊清楚再动手
大厂面试的算法环节,通常给一道leetcode中等难度的题,比如LRU缓存、二叉树遍历、Top K问题。我的策略是:拿到题先不要急着写代码,先跟面试官确认边界条件。比如"输入是null怎么办""数据量多大""允许用额外空间吗"。这一步有两个作用:一是展示你的沟通能力,二是给自己争取思考时间。
想清楚以后,先讲思路再动手写。哪怕思路不是最优解,也要说一句"我先说一个朴素解法,再优化"。面试官要看到的是有逻辑的思维过程,而不是你背下来的最优代码。我见过太多人一上来就默写红黑树,面试官一问"为什么平衡"就卡壳。写一手干净整洁的代码,比写出炫技的解法更加分。
4. 避坑指南:那些写不到简历上的面试经验
面试这个东西,技术实力只占六成,剩下四成是表达、心态、对面试节奏的把握。我踩过不少坑,也总结了一些真实有用的经验,分享给你。
4.1 简历上的每个字,都要能扛住三层追问
简历是面试的唯一入口,写什么内容直接决定了面试官问什么。我的经验是:不要堆砌技术名词,比如"精通Redis""熟悉Netty"这种话,面试官看到"精通"两个字就会往深里挖。写"熟练使用Redis实现分布式缓存,掌握缓存穿透、击穿、雪崩的常见治理方案"反而更安全,因为你能明确知道自己能扛到哪一层。
量化数据一定要真实。写"QPS从500提升到3000",面试官一定会追问你怎么压测的、瓶颈在哪、优化前后对比数据是什么。如果这些数字是你编的,对方只需要问一句"你知道做压测要用什么工具吗"就能戳穿。宁可写保守一点,也不要给面试官留下"虚报"的印象。
4.2 遇到不会的题,别硬编也别冷场
没有谁能在面试里全答上来,遇到不会的问题是常态。我自己的处理套路是:先坦诚说"这个点我了解不深",然后补一句"不过从我对XX的理解来看,它大概和YY有关,我是这么分析的……"。这句话有几个好处:你诚实地承认了知识盲区,又把一个陌生问题拉回到了自己熟悉的领域,面试官能看出你有分析和迁移能力。
最忌讳的是硬编。有一次面试官问我RocketMQ的事务消息实现原理,我其实只了解个大概,结果硬着头皮编了一段"基于本地消息表",当场被面试官拆穿,他直接说"你理解的和RocketMQ官方的实现不太一样"。那次之后我学乖了:不懂就说不懂,顶多扣一点分;不懂装懂,直接出局。
4.3 反问环节:这是最后一次展示的机会
面试结尾面试官通常会说"你有什么想问我的",很多人随口问一句"公司加班多吗"就结束了,等于浪费了最后一次拉好感的机会。我建议问三个方向:团队技术栈和未来规划,比如"咱们团队目前微服务用的是Spring Cloud Alibaba还是自研框架?";或者问业务挑战,比如"秒杀场景下目前最大的痛点是什么?";还可以展示自己的思考,比如"我注意到你们服务是这么拆的,是出于什么考量?"
这些问题能传达一个信号:我不是来找个地方混日子的,我是认真考虑加入这个团队、并且已经对你的业务做了功课的候选人。面试官对这样的人通常印象更深。
4.4 面试节奏与心态管理:别被带偏,要有自己的节奏
大厂面试的连环拷问很容易让人产生"我什么都不懂"的挫败感。我有一轮面试,三个问题连续被追问到底,几乎每个都只能答到七八成,心态差点崩了。后来我刻意调整了策略:当面试官追问到一个我确实答不出来的点,我会主动说"这块我虽然没深入,但我可以讲讲我了解的相邻部分",把话题拉到我能发挥的领域。这不算跑题,反而是一种控场能力。
另外我建议每次面试完,趁着记忆还清晰,花半小时做一个复盘:哪些题答得好、哪些题卡壳了、卡壳的那个知识点是什么。把三个卡壳点记下来,找资料补上,这是比刷十道新题更有效的成长方式。我自己就是靠这个办法,在几次面试之间快速补齐了分布式事务和RocketMQ源码的盲区。
面试本质上是一个双向验证的过程,面试官验证你的技术,你也在验证这个团队值不值得去。抱着这个心态上场,比把面试当成一场审判要轻松得多。下一次如果你也被问到"电商秒杀怎么设计库存",希望你能从容地把那套方案讲透彻——不是我背过,而是我真的想明白了。