ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java面试场景题实战:并发扣库存、慢查询与缓存优化全解析

Java面试场景题实战:并发扣库存、慢查询与缓存优化全解析 在最近的面试复盘和项目沟通中我发现一个很普遍的现象很多 Java 候选人八股文背得滚瓜烂熟但面试官一旦把问题包装成业务场景比如“流量突然涨了十倍接口响应变慢你怎么排查”“并发下单怎么防止超卖”很多人就不知道怎么下手了。这不是个例而是很多自学者真正欠缺的部分知道一个技术点的定义但不清楚它在真实项目里怎么用、为什么这么用、出了问题怎么定位。所以这篇文章我想从“场景题”的角度把 Java 面试中很容易被追问的经典题目拆开讲一遍每一道都结合项目实战思路来展开。不管你是准备校招、社招还是想用项目经验包装自己的简历这套复习思路都值得看完。文章不会只给你一个“标准答案”。我会尽量还原面试官问“为什么”背后的考察点并给出可以落地的代码、SQL 和排查思路。你可以把这篇文章当作一份场景题集训提纲按照里面的模块去复习会比单纯刷题高效很多。1. 为什么 Java 面试越来越喜欢考场景题1.1 八股文面试的局限很多人背了 Java 集合、JVM 内存模型、并发工具类但面试官只要追问一句“你们项目里用到了哪个它解决了什么问题”基本就露馅了。原因在于八股文考察的是“知识有没有见过”而场景题考察的是“遇到问题会不会解决”。企业招人不是找一个考试机器而是希望候选人来了能参与项目开发能处理线上问题。所以面试官正在把问题从“ArrayList 和 LinkedList 的区别”改成“一个接口要支持高并发查询你会选择什么数据结构做缓存”。1.2 场景题到底在考什么场景题表面上是技术问题实际上考察的是三个层次业务理解能力你能不能听懂这个业务场景里真正的痛点在哪里。方案设计能力你选型的时候有没有权衡而不是背出一个名词就完事。工程落地能力你的方案有没有考虑边界条件比如重复请求、网络超时、数据一致性。所以你会发现同样一道题目有工作经验的人回答出来的颗粒度和应届生是不太一样的。差距不在于背得多而在于有没有真的在项目里踩过坑。1.3 怎样才算“结合项目实战”面试官听到“结合项目实战”时想听的不只是你用过 Redis 还是 MySQL而是这个技术在你负责的模块里是怎么引入的。引入前后解决了什么问题带来了什么新的问题。你在做技术方案时有没有对比过其他方案。线上出现过什么异常你是怎么排查和修复的。这种表达如果平时没有积累面试现场很难编出来。所以本文后续的题目我都会按照这个逻辑来演示。2. 场景题的回答框架从“背答案”到“讲方案”在拆解具体题目之前先给出一套通用的回答框架。大多数场景题都可以按四步走第一步确认业务背景和量级。拿到题目先问清楚这个场景的并发量是多少数据量大概什么级别对一致性要求高不高这些问题决定了后面方案的选择。第二步给出主方案并进行对比。先说你的首选方案是什么再简单对比备选方案为什么不用。比如“缓存穿透我会用布隆过滤器空值缓存也能做但布隆过滤器对内存更友好”。第三步描述实现细节和边界条件。包括代码结构、SQL 怎么写、异常怎么处理、重复请求怎么办。这部分越细越能体现实战经验。第四步说明如何监控和排查。比如加了 Redis 缓存之后你怎么知道缓存命中率高不高接口变慢时你怎么定位是 SQL 慢还是 Redis 超时这套框架可以帮助你在紧张的情况下也不会漏掉关键点。接下来我们用它来拆解高频考点。3. 场景题拆解并发扣减库存与超卖问题3.1 面试题还原面试官“假设你们的商品库存只有 10 件但是有 1000 个人同时下单你怎么保证最终卖出去不超过 10 件说说你的方案。”这道题是典型的“并发场景 数据一致性”组合题几乎每家互联网公司面试都会问只是换了个业务壳比如抢票、秒杀、限时抢购。3.2 错误答案是怎么写的很多人第一反应是在 Java 代码里用synchronized或者Lock锁住扣减库存的逻辑。这个思路在单机环境下可以但在微服务集群下完全没用因为多个应用实例各自有一把锁锁不住别的机器。还有人会说“用乐观锁加版本号”。这句话方向对但如果说不出具体 SQL 怎么写、更新失败后怎么办面试官一样不会给你高分。3.3 推荐方案数据库条件更新 库存校验最稳妥的基础方案是在数据库层面完成“判断库存足够且扣减”的原子操作核心是一条 SQLUPDATE t_stock SET stock stock - #{quantity} WHERE product_id #{productId} AND stock #{quantity};这条 SQL 的巧妙之处在于stock #{quantity}条件会在数据库层面拦截超卖。MySQL 执行 update 时会对匹配的行加锁即使 1000 个请求同时进来最终也只有库存够的请求能更新成功。Java 代码只需要判断受影响行数是否为 1Transactional public boolean deductStock(Long productId, Integer quantity) { int updated stockMapper.deductStock(productId, quantity); if (updated 0) { throw new BusinessException(库存不足); } return true; }注意这里必须加Transactional因为后续可能还要写订单记录要保证“扣库存 生成订单”在同一个事务里。这类 SQL 在测试环境验证时没有太大风险但生产库执行 update 前一定要确认 WHERE 条件完整避免无谓锁表。3.4 进阶方案Redis 预扣减如果并发量非常高比如秒杀场景数据库不一定扛得住。这时候常见做法是先用 Redis 做库存预扣减再用异步入库。Lua 脚本可以保证扣减操作原子性local stock tonumber(redis.call(get, KEYS[1])) if not stock or stock tonumber(ARGV[1]) then return -1 end return redis.call(decrby, KEYS[1], ARGV[1])Java 侧调用示例思路如下public boolean preDeductStock(String stockKey, int quantity) { Long result redisTemplate.execute( new DefaultRedisScript(LUA_SCRIPT, Long.class), Collections.singletonList(stockKey), String.valueOf(quantity) ); return result ! null result 0; }这里要注意Redis 预扣减只是一道“流量削峰闸门”最终数据还是要写回 MySQL保证数据库是最终一致性的底座。如果 Redis 扣减成功但订单创建失败需要异步回补库存或者对账补偿。3.5 面试官可能的追问如果扣库存成功了后面创建订单失败怎么办——通过事务回滚或者用补偿消息队列回补库存。用户重复点击下单按钮怎么办——前端按钮置灰 后端幂等校验比如用订单号或用户 ID 做唯一约束。Redis 和 MySQL 库存不一致怎么办——加定时对账任务或者通过消息队列异步同步。4. 场景题拆解MySQL 慢查询排查与索引优化4.1 面试题还原面试官“你负责的订单查询接口刚开始 100ms现在数据量到了几百万变成 3 秒多。你会怎么排查”这道题考察的是候选人有没有真正处理过线上性能问题。回答的时候如果能按“日志定位 → EXPLAIN 分析 → 索引优化 → 分页优化”的流程讲基本就能过关。4.2 第一步开启慢查询日志首先要确认慢的到底是 SQL 还是应用本身最简单的办法是开启 MySQL 慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;这样执行时间超过 1 秒的 SQL 都会记录到日志文件。也可以临时用SHOW PROCESSLIST查看当前正在执行的 SQLSHOW FULL PROCESSLIST;4.3 第二步用 EXPLAIN 分析执行计划找到慢 SQL 后用EXPLAIN查看执行计划重点看几个字段字段关注点type达到 ref 或 range 较好如果是 all 说明全表扫描key实际用到的索引为 NULL 表示没用索引rows预估扫描行数越小越好Extra出现 Using filesort、Using temporary 需要警惕示例EXPLAIN SELECT * FROM t_order WHERE user_id 123 AND status 1 ORDER BY create_time DESC;执行计划里如果type是ref、key是idx_user_status说明索引命中正常。如果type是all就要检查索引有没有建或者有没有索引失效。4.4 第三步索引失效的常见原因即使建了索引SQL 写法不对也会让索引失效列举几个高频场景在索引列上使用函数WHERE DATE(create_time) 2026-01-01隐式类型转换WHERE phone 13800138000如果 phone 是 varchar数字会被转换成字符串导致索引失效前导模糊查询WHERE name LIKE %张%OR 连接非索引列WHERE user_id 123 OR nickname admin正确写法是避免在索引列上做任何计算比如日期范围可以写成SELECT * FROM t_order WHERE create_time 2026-01-01 AND create_time 2026-01-02;4.5 分页查询优化大数据量分页最常见的坑是深分页比如LIMIT 100000, 20需要扫描前面十万条记录再丢弃。经典优化方式是延迟关联先查主键再回表SELECT o.* FROM t_order o INNER JOIN ( SELECT id FROM t_order ORDER BY create_time DESC LIMIT 100000, 20 ) t ON o.id t.id;子查询只查主键列可以利用覆盖索引加速然后再关联回完整记录。4.6 面试加分表达在项目实战中索引优化不是一次做完就结束的。还需要建立慢查询监控定期捞日志把执行计划变化、扫描行数变化记录到文档里。面试时如果能说出“我优化后接口从 3 秒降到了 200ms并且业务高峰期没有再出现慢 SQL”说服力会强很多。5. 场景题拆解Redis 缓存穿透、击穿、雪崩5.1 面试题还原面试官“你们项目里 Redis 都拿来做缓存对吧如果某个热点商品的详情页突然被大量请求缓存里又没有数据会发生什么你怎么解决”这道题把三个经典问题打包在一起缓存穿透、缓存击穿、缓存雪崩。面试官不需要你背定义而是想听你怎么设计一套应对高并发读的方案。5.2 缓存穿透缓存穿透是指查询一个根本不存在的数据缓存和数据库都没有导致请求每次都打到数据库。恶意攻击者只要构造一批不存在的 ID就能把数据库拖垮。常用解决方案有两种第一种是缓存空值。查询结果为空时也往 Redis 里写一个空值设置较短的过期时间比如 3 到 5 分钟。第二种是布隆过滤器。启动时把数据库里存在的 ID 加载到布隆过滤器里请求进来先判断 ID 是否存在不存在直接返回没有机会打到数据库。5.3 缓存击穿缓存击穿是某个热点 key 过期的一瞬间大量请求同时打到数据库。和穿透的区别在于这个 key 本身是存在的只是缓存的过期时间到了。解决方案是互斥锁。在缓存没有命中的时候只有一个线程去数据库加载其他线程短暂等待public String getProductInfo(String productId) { String cacheValue redisTemplate.opsForValue().get(productId); if (cacheValue ! null) { return cacheValue; } // 尝试获取锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock: productId, 1, Duration.ofSeconds(10)); if (Boolean.TRUE.equals(locked)) { try { cacheValue redisTemplate.opsForValue().get(productId); if (cacheValue ! null) { return cacheValue; } String dbValue queryFromDb(productId); redisTemplate.opsForValue().set(productId, dbValue, Duration.ofMinutes(30)); return dbValue; } finally { redisTemplate.delete(lock: productId); } } // 没拿到锁的线程短暂睡眠后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getProductInfo(productId); }这里两次查询 Redis 是为了做双重检查避免锁拿到后查询接口还没执行完、旧数据已经过期的问题。5.4 缓存雪崩缓存雪崩是大规模 key 在同一时间失效导致数据库压力瞬间飙升。最直接的解决办法是让过期时间分散开int expireTime 1800 new Random().nextInt(300); redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(expireTime));另一种思路是热点数据不设置过期时间改成由后台定时任务主动刷新相当于把“被动过期”改成“主动更新”。5.5 结合项目实战怎么讲比如在停车场车牌识别项目里车牌基本信息、会员套餐信息都可以缓存。一台相机识别到车牌后业务服务需要快速判断这个车牌是不是月卡车、剩余时长多少。如果不加缓存每次抬杆都要查一遍数据库车多的时候数据库压力很大加缓存后响应速度会快很多这也是一个可以写进简历的亮点。6. 场景题拆解消息队列削峰与最终一致性6.1 面试题还原面试官“用户量一大很多操作不能同步完成了你会怎么设计异步化怎么保证消息不丢、不重复消费”消息队列几乎是后端面试必问Kafka 又是最常见的选项。场景题通常会围绕“异步解耦、削峰填谷、最终一致性”来展开。6.2 消息队列在项目里解决什么问题最常见的例子是订单创建后的流程。如果不引入 MQ用户点击下单后业务逻辑要串行完成扣库存、发优惠券、发短信、推送消息、更新积分。任何一个环节慢用户都要等着。引入 MQ 之后只需要完成核心链路扣库存、创建订单然后发送一条“订单创建成功”的消息其余流程交给消费者异步处理。6.3 怎么保证消息不丢失消息丢失通常发生在三个位置生产者发送时、Broker 存储时、消费者消费时。生产者端开启 Kafka 生产者确认机制acksall表示分区副本全部写入成功才返回成功。Broker 端设置replication.factor大于 1同时min.insync.replicas设置为 2 左右保证有副本同步。消费者端关闭自动提交位移处理成功后再手动提交offset。Java 消费者手动提交的示例思路while (true) { ConsumerRecordsString, String records consumer.poll(Duration.ofMillis(1000)); for (ConsumerRecordString, String record : records) { try { processRecord(record); } catch (Exception e) { // 记录异常并告警避免无限重试 log.error(process message error, e); continue; } consumer.commitSync(); } }6.4 怎么保证不重复消费消息队列做不到严格的一次消费只能做到“业务上幂等”。消费者要做的是不管消息重复多少次业务结果都一样。常见的幂等方案数据库唯一约束消费前先 insert 一条消息记录唯一键冲突说明已经处理过。Redis 处理标记用setIfAbsent给消息 ID 加一个处理标识。业务状态判断比如订单已支付再次收到支付回调就直接跳过。6.5 面试加分表达在停车场项目里车牌识别结果可以设计成 MQTT 消息推到业务服务再转发到 Kafka 做异步处理比如生成入场记录、推送公众号通知、同步计费系统。面试时讲到这一点既能体现你用过 MQTT 设备对接又能体现异步处理的工程思维。7. 场景题拆解JVM 内存溢出OOM线上排查7.1 面试题还原面试官“如果线上应用突然频繁 Full GC或者直接抛出OutOfMemoryError: Java heap space你会怎么排查”这道题是高级开发岗位的高频题也会在校招面试里作为拓展题出现。掌握一套固定的排查流程即使没有真实经历过也能表现出清晰的思路。7.2 不要猜先看日志和监控第一步是看日志找到报错堆栈确认是堆内存溢出、元空间溢出还是栈溢出。常见报错类型java.lang.OutOfMemoryError: Java heap space堆内存不够。java.lang.OutOfMemoryError: Metaspace加载的类元数据过多。java.lang.StackOverflowError递归调用太深。同时查看监控面板里的内存曲线、GC 频率。如果 GC 越来越频繁且回收效果差多半存在大对象或内存泄漏。7.3 保留现场导出堆 dump启动参数建议提前加上 OOM 时自动 dump-Xms512m -Xmx512m -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/heapdump.hprof线上出现 OOM 后用 MAT 打开导出的 hprof 文件查看 Dominator Tree找占用内存最大的对象然后逐个检查它被谁引用。7.4 常见代码原因下面这些场景很容易在高并发下触发 OOM集合对象被 static 字段持有只往里加不清理。循环里不断创建大数组比如byte[]。数据库查出的结果集没有限制大小。线程池队列设置太长任务积压占满内存。举一个比较容易复现的代码例子public class OomDemo { private static final Listbyte[] CACHE new ArrayList(); public void add() { while (true) { CACHE.add(new byte[1024 * 1024]); } } }这个案例在本地运行时很快会抛出堆内存溢出原因是静态集合持有大量对象GC 无法回收。这里强调的是排查思路具体内存大小要根据你的 JVM 参数决定。7.5 排查 checkList排查步骤关键动作看报错信息确认是堆内存、元空间还是栈溢出确认 JVM 参数-Xms、-Xmx是否合理分析 GC 日志是否存在频繁 Full GC导出 heap dump用 MAT 或 JProfiler 分析定位对象引用检查大对象和静态容器修复后验证小流量发布观察内存曲线8. 项目实战怎么讲用 MQTT 对接车牌识别相机的面试思路8.1 为什么这个项目话题容易加分现在很多公司做智慧停车、智慧园区项目都会遇到海康、大华等主流车牌识别相机的对接需求。相机把抓拍到的车牌数据通过 MQTT 上报业务系统基于这些数据做收费、放行、查询。这个场景涉及网络通信、消息可靠性、业务落库和前端联动非常适合拿来作为项目实战亮点。8.2 技术选型为什么用 MQTT 而不是 HTTP车牌识别相机是嵌入式设备通常不具备完整的 HTTP 服务端能力但它支持 MQTT 协议。MQTT 基于发布订阅模型适合设备端和平台端保持长连接、实时上报数据的场景而且自带 QoS 机制可以解决消息丢失问题。一般的设计是相机识别到车牌后发布消息到某个 topic业务服务订阅对应 topic解析消息体然后执行计费业务。8.3 核心代码示例以 Eclipse Paho 客户端为例演示订阅车牌识别结果的核心思路dependency groupIdorg.eclipse.paho/groupId artifactIdorg.eclipse.paho.client.mqttv3/artifactId version按项目实际版本填写/version /dependency订阅端String broker tcp://127.0.0.1:1883; String clientId parking-server- System.currentTimeMillis(); MqttClient client new MqttClient(broker, clientId, new MemoryPersistence()); MqttConnectOptions options new MqttConnectOptions(); options.setAutomaticReconnect(true); options.setCleanSession(true); options.setConnectionTimeout(10); client.setCallback(new MqttCallback() { Override public void connectionLost(Throwable cause) { // 自动重连会触发记录日志并告警 log.error(mqtt connection lost, cause); } Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload new String(message.getPayload(), StandardCharsets.UTF_8); // 解析车牌号和相机编号写入业务处理 handlePlateMessage(topic, payload); } Override public void deliveryComplete(IMqttDeliveryToken token) { // 发布消息成功回调订阅场景一般不处理 } }); client.connect(options); client.subscribe(parking/plate//result, 1);8.4 面试官会追问什么Topic 怎么设计比如parking/{相机编号}/result方便按相机隔离。QoS 怎么选车牌识别消息不能丢建议 QoS 1至少一次投递。重复消息怎么处理业务侧做幂等比如用车牌号和入场时间做唯一约束。相机离线了怎么办监控连接状态断线告警同时保留本地缓存。高并发时怎么削峰相机消息进入 Kafka业务服务异步消费落库。这些问题如果都能答上来项目实战这块就很扎实了。9. 常见面试问题避坑清单9.1 面试官问“项目里遇到的最大问题是什么”怎么答不要只说“遇到了 bug百度解决了”。最好用 STAR 法则讲清楚背景、任务、行动、结果。比如“在对接车牌识别相机时发现高峰期出现消息丢失我通过分析 MQTT QoS 配置和消费者幂等逻辑最终解决了问题”。9.2 讲项目时怎么避免“像背稿”不要按简历上写的功能逐条罗列而是围绕 2 到 3 个难点展开。每个难点都讲技术选型、具体实现、遇到问题、解决方案讲完再总结收获。9.3 遇到不会的场景题怎么办先思考不要急着说“不会”。可以从问题里抽取出你熟悉的技术点比如面试官问分布式锁你不知道 Redisson 的底层实现但你知道 RedisSETNX就可以顺着这个思路说下去。面试官看重的是解决问题的路径而不是标准答案本身。9.4 源码到底要不要背不建议背源码。更好的方式是理解关键类的设计思想比如 HashMap 的扩容机制、ConcurrentHashMap 的锁粒度变化、Spring 的 Bean 生命周期。能用自己的话讲清楚设计思想比默写源码更能打动人。10. 场景题复习路线建议准备 Java 场景题不要漫无目的地刷题。建议按下面这个顺序过一遍第一把 Java 基础语法和集合类再过一遍尤其是 HashMap、ConcurrentHashMap、ArrayList 的底层设计和适用场景。第二把并发相关的知识串起来包括 synchronized、ReentrantLock、volatile、线程池参数设置、并发工具类。面试里提问频率很高而且很容易和场景题组合。第三数据库是重头戏索引、事务隔离级别、锁机制、慢查询排查每一块都值得熟悉。第四Redis 和消息队列是项目实战加分项重点理解缓存一致性、缓存雪崩、消息可靠性、幂等设计。第五JVM 相关掌握内存区域、GC 算法、OOM 排查思路尤其是能结合线上的案例来展开。第六复盘你自己做过的项目哪怕只是课设也要能把技术难点说清楚。简历上写的每一个技术点都提前想好面试官会围绕它问什么。场景题复习的核心不是“我看过多少篇文章”而是“我能不能把知识点串联成一个可落地的方案”。建议你面试前几天找一个安静的时间把本文提到的几个场景题自己写一遍答案最好能写出 SQL 或 Java 伪代码然后对镜子或者找朋友模拟讲一遍。你会发现写出来和讲出来是完全不同的难度提前练过之后面试现场会从容很多。
返回列表