【Java面试】场景题

By | 2026 年 6 月 18 日

线上故障排查类

线上服务突然CPU飙升到100%,但GC次数正常,如何快速定位?

  • 核心排查思路(黄金5分钟) :
    1. top -H -p <pid> 查看进程中哪个线程(LWP) 消耗CPU最高,记录线程ID(十进制)。
    2. printf "%x\n" <线程ID> 转换为十六进制。
    3. jstack <pid> | grep -A 20 <十六进制线程ID> 查看该线程栈。
    4. 根因分析(四种常见情况) :
      • 死循环/空轮询:如 while(true) 未设置休眠,多见于Netty的NioEventLoop bug或业务自旋锁。
      • 频繁Full GC(但你说GC正常,排除)
      • 正则表达式回溯:如 (a+)+b 匹配超长字符串,导致CPU飙高,通过jstack定位到Pattern匹配行。
      • 序列化/反序列化:如Jackson解析超大JSON,占用大量CPU计算。
  • 补充工具arthas 的 thread -n 3 一键抓取最忙的3个线程,比手动jstack效率高10倍。

系统频繁发生FGC,但堆内存(Heap)占用很低,可能是什么原因?

面试官陷阱:绝大多数人只会看堆内存,忽视了非堆内存(Metaspace/DirectBuffer)

  • 根因排查清单
    1. Metaspace(元空间)膨胀:大量动态生成类(CGLIB代理、Groovy脚本、JSP编译)导致Metaspace阈值触发FGC。使用 jstat -gc <pid> 查看 MC(当前Metaspace容量)和 MU(已使用),若接近 MaxMetaspaceSize 则确证。
    2. Direct Memory(直接内存)泄漏:NIO(Netty)使用 ByteBuffer.allocateDirect 未释放,虽然不在Heap内,但FGC会尝试回收DirectMemory。使用 jcmd <pid> VM.native_memory 查看。
    3. System.gc() 显式调用:某些框架(RMI、NIO)会主动触发FGC,加 -XX:+DisableExplicitGC 屏蔽(但谨慎)。
    4. 堆外内存不足-XX:MaxDirectMemorySize 设置过小。

线上数据库连接池突然爆满(Active Connections飙高),应用无响应,如何排查?

  • 排查步骤
    1. 先摘流量(网关切流或K8s删Pod),防止故障蔓延。
    2. jstack 导出线程栈,搜索 getConnection 或 datasource 关键字,看哪些线程在等待获取连接。
    3. 检查慢SQL:是否在事务中调用了第三方RPC接口(如HTTP超时),导致数据库连接一直不释放。
    4. 检查连接池配置
      • maxActive 是否合理(通常50~100)。
      • maxWait 是否配置(若为-1无限等待,线程全部阻塞)。
      • 是否忘了在finally中关闭Connection(虽然Druid/Hikari有泄露检测,需开启 leakDetectionThreshold)。
  • 紧急止损:重启应用(瞬时释放连接),然后临时调大 maxActive,事后优化慢SQL或拆分长事务。

高并发与分布式架构类

设计一个“百万级QPS”的签到/点赞计数器,如何保证数据绝对不丢且延迟小于5ms?

  • 方案设计(多级缓存 + 异步刷盘) :
    1. L1(本地内存) :每个JVM实例内使用 ConcurrentHashMap + 定时聚合(如每1秒)。写请求直接操作内存中的原子变量(LongAdder),响应在微秒级。
    2. L2(Redis) :本地聚合后的增量数据,使用Lua脚本原子累加到Redis(INCRBY)。
    3. 异步落库:Redis中的计数器值,通过定时任务(每5秒)或MQ 异步写入DB(或HBase)。
  • 防丢机制
    • 本地内存聚合时,记录批次ID(BatchId)。若JVM突然宕机,重启后对比Redis中的最新值,未落库的批次从Redis反查补偿(依赖Redis AOF持久化)。
    • 使用双缓冲队列(一个写,一个刷),刷盘时交换指针,避免加锁。

RocketMQ/Kafka消费时,因下游DB抖动导致消费积压,且已有大量消息在队列中,如何快速恢复?

  • 止损三步走
    1. 临时扩容消费者实例(前提是Topic的分区数 > 消费者数)。若分区数固定无法扩容,启动临时应急消费者,只做消息ack,将消息转发至新的临时Topic,用更多的分区数并行处理。
    2. 开启批量处理:消费者改为拉取一批消息(如一次拉取1000条),使用批量SQL(如Batch Insert/Update),极大减少DB交互次数。
    3. 降级非核心逻辑:如发送短信、写入日志ES等操作暂时关闭或设置为异步丢弃。
  • 事后根治:分析下游DB慢的原因(索引缺失/锁竞争),增加DB从库读写分离。

分布式锁(Redis)在极端情况下(主从切换)失效,导致超卖,如何兜底?

  • 问题复现:线程A在Master获取锁(SET key value NX EX 30),Master宕机未同步到Slave,Slave升级为Master,线程B获取到了相同的锁,导致超卖。
  • 兜底方案(DB层面最终防御) :
    1. 数据库乐观锁是最后一道防线。扣库存SQL必须加条件:UPDATE inventory SET amount = amount - #{buyNum} WHERE product_id = #{id} AND amount >= #{buyNum}
    2. 若影响行数为0,抛异常回滚业务,人工补偿或触发重试。
  • 关于RedLock的讨论(加分项) :
    • 面试官若问及RedLock(红锁),需指出Martin Kleppmann的批判:红锁强依赖系统时钟同步(NTP) ,且跨机房网络延迟会导致活锁。在大厂实践中,绝大多数场景下单Redis主从 + DB乐观锁 已足够,引入RedLock成本远大于收益。

数据库与缓存一致性类

缓存(Redis)和数据库(MySQL)双写一致性,如何保证?延迟双删策略存在什么缺陷?

  • 经典方案Cache-Aside(旁路缓存) 模式 + 延迟双删
    • 写操作:更新DB → 删除Redis缓存(等待500ms后再次删除,防止并发读将旧数据写回)。
  • 延迟双删缺陷
    • 第二次删除延迟时间难以预估(业务SQL执行时间飘忽不定),若时间太短,第二次删除来不及,脏数据依然存在。
    • 若第二次删除失败(Redis超时),没有任何补偿机制。
  • 终极方案(大厂主流) :
    • 订阅 Canal(MySQL Binlog增量解析),将变更事件发送至MQ,消费者异步删除/更新Redis。
    • 优点:解耦、保证最终一致性,且不会污染业务主流程(主流程只管更新DB和发送MQ即可)。
    • 注意:缓存读取逻辑需容忍短暂不一致(如设置过期时间兜底,例如5分钟自动失效)。

分库分表后,如何生成全局唯一的分布式ID?雪花算法(Snowflake)遇到时钟回拨怎么办?

  • 常见方案:雪花算法(64位:符号位1 + 时间戳41 + 机器ID10 + 序列号12)。
  • 时钟回拨处理(重点) :
    1. 记录上次生成ID的时间戳(lastTimestamp) 。若当前时间 < lastTimestamp,判定为时钟回拨。
    2. 保守策略:抛出异常,通知运维人工校准时钟(大厂标准做法)。
    3. 等待策略Thread.sleep(差值),等待时钟追上(仅适用于回拨毫秒级,若回拨数秒则不可行)。
    4. 备用方案:使用ZooKeeper持久化序列号数据库号段模式(Leaf-segment) 作为兜底。
  • 面试加分:美团的 Leaf 方案(号段双Buffer),每次获取一个号段(如1000-2000),用完再取,不依赖时钟,性能极高且绝对不重复。

微服务治理类

线上Feign调用频繁超时,如何快速定位是网络问题、服务端处理慢还是GC问题?

  • 分层排查法(网络 → 服务端 → JVM) :
    1. 网络层:在调用方执行 telnet <ip> <port> 或 ping,确认网络连通性。使用 tcpdump 抓包分析SYN重传。
    2. 服务端线程池:查看服务端 jstack,是否有大量线程在 WAITING(如数据库连接池耗尽)。
    3. 服务端GCjstat -gcutil 查看GC频率,若FGC频繁,STW导致响应变慢。
    4. RPC层监控:查看Skywalking/Zipkin的Trace,定位具体哪一步耗时最长(如下游调下游)。
  • 临时止损:调大Feign的超时时间(connectTimeout / readTimeout),开启Feign重试(注意幂等性),或快速熔断返回降级数据。

Spring Cloud Gateway网关通过Sentinel限流,如何做到“根据用户等级(VIP/普通)”差异化限流?

  • 核心思路:使用Sentinel的热点参数限流(ParamFlow) 。
    1. 自定义 RequestOriginParser 解析请求头中的 userId,查询用户等级(VIP/普通)。
    2. 将用户等级作为参数传递给Sentinel(SphU.entry("resourceName", EntryType.IN, 1, userLevel))。
    3. 配置热点规则:对参数 userLevel 分别设置阈值,VIP用户QPS=1000,普通用户QPS=100。
  • 进阶:若用户量极大,本地缓存用户等级(Caffeine),每5分钟同步一次,避免每次限流都查DB/Redis造成性能损耗。

Spring Boot 3.x 使用AOT编译和GraalVM原生镜像后,哪些功能会受限?

*这是2025-2026年的新兴热点。*

  • 受限功能
    1. 动态代理(CGLIB/JDK Proxy) :GraalVM静态编译时无法提前确定代理类,需在 reflect-config.json 中预声明。
    2. 反射/内省:所有反射调用必须在编译时注册(如Jackson序列化、MyBatis实体类)。
    3. Resource文件加载ClassPathResource 需显式在 resource-config.json 中配置。
  • 解决方案:Spring Boot 3.x提供了 @Hint 注解和 AOT Agent(运行测试时捕获动态调用),生成配置文件。迁移成本较高,适合云原生Serverless场景(启动毫秒级),传统微服务慎用。

设计一个秒杀系统的库存扣减方案。

  • 架构分层
    1. 流量削峰:前端秒杀按钮置灰 + 验证码(防止脚本刷);网关层限流(令牌桶,如 Sentinel)。
    2. 库存预扣(Redis)
      • 使用 Lua 脚本 保证原子性:if (redis.call('get', key) > 0) then redis.call('decr', key) return 1 else return 0 end
      • 此处为预扣(逻辑库存),不直接操作 DB,响应毫秒级。
    3. 异步落库
      • 预扣成功后,生成订单消息发送至 RocketMQ/Kafka,返回客户端“排队中”。
      • 消费者拉取消息,执行 DB 扣减真实库存(使用 乐观锁update inventory set amount=amount-? where id=? and amount>=?,若影响行数为0则记录失败)。
    4. 防超卖兜底
      • Redis 扣减失败直接返回“已售罄”。
      • 数据库扣减失败触发 补偿回滚(将 Redis 库存 +1),通过消息重试或定时任务保证最终一致性。

分库分表后如何实现分页查询?

  • 挑战limit 10, 10 在分库分表下需要每个分片都查询 20 条数据,然后归并排序取第 10~20 条,数据量越大性能越差(ShardingSphere 默认支持但伴随性能瓶颈)。
  • 解决方案
    1. 禁止跳页(游标滚动):业务上改为“上一页/下一页”,使用 where id > lastId order by id limit 10。每条 SQL 在各分片查询,归并取最小 10 条即可,性能极高,适合瀑布流。
    2. ES 宽表冗余:将需要分页展示的字段同步到 ElasticSearch,依靠 ES 的分布式分页(from/size 或 search_after)完成。
    3. 二次查询法(ShardingSphere 默认优化):先查询到目标数据的主键 ID(例如 limit 1000,10,实际每条 SQL 只查 ID),再根据 ID 回表查询完整数据。
    4. 数仓/汇总库:将分表数据按小时同步到一个只读的汇总库(单库),在该库进行分页(适合报表类慢查询)。

突发流量导致订单接口超时,如何10分钟内止损?

止损黄金十分钟(止血 > 排查)

  1. 扩容:快速增加 Pod 实例(K8s HPA 或手动调整副本数),或临时增加物理机/容器资源。
  2. 限流与熔断
    • 在网关(如 Sentinel / Hystrix)配置快速失败QPS 限流,超出返回友好提示“系统繁忙”)。
    • 开启熔断(错误率 > 50% 自动熔断,走 fallback 兜底返回缓存数据)。
  3. 降级:非核心逻辑(如发送短信、更新积分)改为异步 MQ 处理,或直接关闭这些功能。
  4. 慢查询定位
    • 连接 APM(Skywalking/Pinpoint)查看 Trace 链路,定位慢 SQL 或第三方 RPC 调用。
    • 若发现慢 SQL,快速执行 执行计划优化(加索引)或 限流 SQL(SQL 网关层拦截)。
  5. JVM 紧急处理:若 CPU 飙升,jstack 查看是否大量 GC;若内存高,考虑 jmap 导出并调整 -Xmx 参数重启(临时方案)。
  6. 消息队列解压:若下游处理慢,将请求直接投入 MQ 排队,立即返回“处理中”,异步通知结果。

如何设计一个日500万次搜索的商品检索系统?

*注:500万次/天 ≈ 58 QPS,高峰期(10倍)约 580 QPS,属于中等规模。*

  • 技术选型Elasticsearch 集群(倒排索引 + 分词器,如 IK)。
  • 架构设计
    1. 索引拆分:按商品类目或时间分索引(index-{date}),便于冷热数据分离。
    2. 分片策略:设置合理的 number_of_shards(约 数据总量 / 单分片建议大小(10~50GB)),副本数至少 1 个(保证高可用)。
    3. 写入链路:商品变更通过 Canal 监听 MySQL Binlog → 发送至 MQ → 消费者增量更新 ES(保证最终一致性)。
    4. 查询链路
      • L1 本地缓存(Caffeine):缓存高热度搜索词(Top 1000 关键词)的返回结果,TTL 5min。
      • L2 Redis 缓存:缓存次热词,TTL 1min。
      • L3 ES 查询:使用 bool 组合查询 + function_score 按销量/评分自定义相关度。
    5. 性能优化
      • 禁用 * 通配符查询,使用精确字段(keyword)聚合。
      • 开启 routing 路由,确保相同类目的商品落在同一分片,减少跨分片查询。
      • 配置 refresh_interval 调大(如 30s),降低写入压力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注