线上故障排查类
线上服务突然CPU飙升到100%,但GC次数正常,如何快速定位?
- 核心排查思路(黄金5分钟) :
top -H -p <pid>查看进程中哪个线程(LWP) 消耗CPU最高,记录线程ID(十进制)。printf "%x\n" <线程ID>转换为十六进制。jstack <pid> | grep -A 20 <十六进制线程ID>查看该线程栈。- 根因分析(四种常见情况) :
- 死循环/空轮询:如
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)。
- 根因排查清单:
- Metaspace(元空间)膨胀:大量动态生成类(CGLIB代理、Groovy脚本、JSP编译)导致Metaspace阈值触发FGC。使用
jstat -gc <pid>查看MC(当前Metaspace容量)和MU(已使用),若接近MaxMetaspaceSize则确证。 - Direct Memory(直接内存)泄漏:NIO(Netty)使用
ByteBuffer.allocateDirect未释放,虽然不在Heap内,但FGC会尝试回收DirectMemory。使用jcmd <pid> VM.native_memory查看。 System.gc()显式调用:某些框架(RMI、NIO)会主动触发FGC,加-XX:+DisableExplicitGC屏蔽(但谨慎)。- 堆外内存不足:
-XX:MaxDirectMemorySize设置过小。
- Metaspace(元空间)膨胀:大量动态生成类(CGLIB代理、Groovy脚本、JSP编译)导致Metaspace阈值触发FGC。使用
线上数据库连接池突然爆满(Active Connections飙高),应用无响应,如何排查?
- 排查步骤:
- 先摘流量(网关切流或K8s删Pod),防止故障蔓延。
jstack导出线程栈,搜索getConnection或datasource关键字,看哪些线程在等待获取连接。- 检查慢SQL:是否在事务中调用了第三方RPC接口(如HTTP超时),导致数据库连接一直不释放。
- 检查连接池配置:
maxActive是否合理(通常50~100)。maxWait是否配置(若为-1无限等待,线程全部阻塞)。- 是否忘了在finally中关闭Connection(虽然Druid/Hikari有泄露检测,需开启
leakDetectionThreshold)。
- 紧急止损:重启应用(瞬时释放连接),然后临时调大
maxActive,事后优化慢SQL或拆分长事务。
高并发与分布式架构类
设计一个“百万级QPS”的签到/点赞计数器,如何保证数据绝对不丢且延迟小于5ms?
- 方案设计(多级缓存 + 异步刷盘) :
- L1(本地内存) :每个JVM实例内使用
ConcurrentHashMap+ 定时聚合(如每1秒)。写请求直接操作内存中的原子变量(LongAdder),响应在微秒级。 - L2(Redis) :本地聚合后的增量数据,使用Lua脚本原子累加到Redis(
INCRBY)。 - 异步落库:Redis中的计数器值,通过定时任务(每5秒)或MQ 异步写入DB(或HBase)。
- L1(本地内存) :每个JVM实例内使用
- 防丢机制:
- 本地内存聚合时,记录批次ID(BatchId)。若JVM突然宕机,重启后对比Redis中的最新值,未落库的批次从Redis反查补偿(依赖Redis AOF持久化)。
- 使用双缓冲队列(一个写,一个刷),刷盘时交换指针,避免加锁。
RocketMQ/Kafka消费时,因下游DB抖动导致消费积压,且已有大量消息在队列中,如何快速恢复?
- 止损三步走:
- 临时扩容消费者实例(前提是Topic的分区数 > 消费者数)。若分区数固定无法扩容,启动临时应急消费者,只做消息
ack,将消息转发至新的临时Topic,用更多的分区数并行处理。 - 开启批量处理:消费者改为拉取一批消息(如一次拉取1000条),使用批量SQL(如Batch Insert/Update),极大减少DB交互次数。
- 降级非核心逻辑:如发送短信、写入日志ES等操作暂时关闭或设置为异步丢弃。
- 临时扩容消费者实例(前提是Topic的分区数 > 消费者数)。若分区数固定无法扩容,启动临时应急消费者,只做消息
- 事后根治:分析下游DB慢的原因(索引缺失/锁竞争),增加DB从库读写分离。
分布式锁(Redis)在极端情况下(主从切换)失效,导致超卖,如何兜底?
- 问题复现:线程A在Master获取锁(
SET key value NX EX 30),Master宕机未同步到Slave,Slave升级为Master,线程B获取到了相同的锁,导致超卖。 - 兜底方案(DB层面最终防御) :
- 数据库乐观锁是最后一道防线。扣库存SQL必须加条件:
UPDATE inventory SET amount = amount - #{buyNum} WHERE product_id = #{id} AND amount >= #{buyNum}。 - 若影响行数为0,抛异常回滚业务,人工补偿或触发重试。
- 数据库乐观锁是最后一道防线。扣库存SQL必须加条件:
- 关于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)。
- 时钟回拨处理(重点) :
- 记录上次生成ID的时间戳(lastTimestamp) 。若当前时间 < lastTimestamp,判定为时钟回拨。
- 保守策略:抛出异常,通知运维人工校准时钟(大厂标准做法)。
- 等待策略:
Thread.sleep(差值),等待时钟追上(仅适用于回拨毫秒级,若回拨数秒则不可行)。 - 备用方案:使用ZooKeeper持久化序列号或数据库号段模式(Leaf-segment) 作为兜底。
- 面试加分:美团的 Leaf 方案(号段双Buffer),每次获取一个号段(如1000-2000),用完再取,不依赖时钟,性能极高且绝对不重复。
微服务治理类
线上Feign调用频繁超时,如何快速定位是网络问题、服务端处理慢还是GC问题?
- 分层排查法(网络 → 服务端 → JVM) :
- 网络层:在调用方执行
telnet <ip> <port>或ping,确认网络连通性。使用tcpdump抓包分析SYN重传。 - 服务端线程池:查看服务端
jstack,是否有大量线程在WAITING(如数据库连接池耗尽)。 - 服务端GC:
jstat -gcutil查看GC频率,若FGC频繁,STW导致响应变慢。 - RPC层监控:查看Skywalking/Zipkin的Trace,定位具体哪一步耗时最长(如下游调下游)。
- 网络层:在调用方执行
- 临时止损:调大Feign的超时时间(
connectTimeout/readTimeout),开启Feign重试(注意幂等性),或快速熔断返回降级数据。
Spring Cloud Gateway网关通过Sentinel限流,如何做到“根据用户等级(VIP/普通)”差异化限流?
- 核心思路:使用Sentinel的热点参数限流(ParamFlow) 。
- 自定义
RequestOriginParser解析请求头中的userId,查询用户等级(VIP/普通)。 - 将用户等级作为参数传递给Sentinel(
SphU.entry("resourceName", EntryType.IN, 1, userLevel))。 - 配置热点规则:对参数
userLevel分别设置阈值,VIP用户QPS=1000,普通用户QPS=100。
- 自定义
- 进阶:若用户量极大,本地缓存用户等级(Caffeine),每5分钟同步一次,避免每次限流都查DB/Redis造成性能损耗。
Spring Boot 3.x 使用AOT编译和GraalVM原生镜像后,哪些功能会受限?
*这是2025-2026年的新兴热点。*
- 受限功能:
- 动态代理(CGLIB/JDK Proxy) :GraalVM静态编译时无法提前确定代理类,需在
reflect-config.json中预声明。 - 反射/内省:所有反射调用必须在编译时注册(如Jackson序列化、MyBatis实体类)。
- Resource文件加载:
ClassPathResource需显式在resource-config.json中配置。
- 动态代理(CGLIB/JDK Proxy) :GraalVM静态编译时无法提前确定代理类,需在
- 解决方案:Spring Boot 3.x提供了
@Hint注解和 AOT Agent(运行测试时捕获动态调用),生成配置文件。迁移成本较高,适合云原生Serverless场景(启动毫秒级),传统微服务慎用。
设计一个秒杀系统的库存扣减方案。
- 架构分层:
- 流量削峰:前端秒杀按钮置灰 + 验证码(防止脚本刷);网关层限流(令牌桶,如 Sentinel)。
- 库存预扣(Redis):
- 使用 Lua 脚本 保证原子性:
if (redis.call('get', key) > 0) then redis.call('decr', key) return 1 else return 0 end。 - 此处为预扣(逻辑库存),不直接操作 DB,响应毫秒级。
- 使用 Lua 脚本 保证原子性:
- 异步落库:
- 预扣成功后,生成订单消息发送至 RocketMQ/Kafka,返回客户端“排队中”。
- 消费者拉取消息,执行 DB 扣减真实库存(使用 乐观锁:
update inventory set amount=amount-? where id=? and amount>=?,若影响行数为0则记录失败)。
- 防超卖兜底:
- Redis 扣减失败直接返回“已售罄”。
- 数据库扣减失败触发 补偿回滚(将 Redis 库存 +1),通过消息重试或定时任务保证最终一致性。
分库分表后如何实现分页查询?
- 挑战:
limit 10, 10在分库分表下需要每个分片都查询 20 条数据,然后归并排序取第 10~20 条,数据量越大性能越差(ShardingSphere 默认支持但伴随性能瓶颈)。 - 解决方案:
- 禁止跳页(游标滚动):业务上改为“上一页/下一页”,使用
where id > lastId order by id limit 10。每条 SQL 在各分片查询,归并取最小 10 条即可,性能极高,适合瀑布流。 - ES 宽表冗余:将需要分页展示的字段同步到 ElasticSearch,依靠 ES 的分布式分页(
from/size或search_after)完成。 - 二次查询法(ShardingSphere 默认优化):先查询到目标数据的主键 ID(例如 limit 1000,10,实际每条 SQL 只查 ID),再根据 ID 回表查询完整数据。
- 数仓/汇总库:将分表数据按小时同步到一个只读的汇总库(单库),在该库进行分页(适合报表类慢查询)。
- 禁止跳页(游标滚动):业务上改为“上一页/下一页”,使用
突发流量导致订单接口超时,如何10分钟内止损?
止损黄金十分钟(止血 > 排查):
- 扩容:快速增加 Pod 实例(K8s HPA 或手动调整副本数),或临时增加物理机/容器资源。
- 限流与熔断:
- 在网关(如 Sentinel / Hystrix)配置快速失败(
QPS限流,超出返回友好提示“系统繁忙”)。 - 开启熔断(错误率 > 50% 自动熔断,走
fallback兜底返回缓存数据)。
- 在网关(如 Sentinel / Hystrix)配置快速失败(
- 降级:非核心逻辑(如发送短信、更新积分)改为异步 MQ 处理,或直接关闭这些功能。
- 慢查询定位:
- 连接 APM(Skywalking/Pinpoint)查看 Trace 链路,定位慢 SQL 或第三方 RPC 调用。
- 若发现慢 SQL,快速执行 执行计划优化(加索引)或 限流 SQL(SQL 网关层拦截)。
- JVM 紧急处理:若 CPU 飙升,
jstack查看是否大量 GC;若内存高,考虑jmap导出并调整-Xmx参数重启(临时方案)。 - 消息队列解压:若下游处理慢,将请求直接投入 MQ 排队,立即返回“处理中”,异步通知结果。
如何设计一个日500万次搜索的商品检索系统?
*注:500万次/天 ≈ 58 QPS,高峰期(10倍)约 580 QPS,属于中等规模。*
- 技术选型:Elasticsearch 集群(倒排索引 + 分词器,如 IK)。
- 架构设计:
- 索引拆分:按商品类目或时间分索引(
index-{date}),便于冷热数据分离。 - 分片策略:设置合理的
number_of_shards(约数据总量 / 单分片建议大小(10~50GB)),副本数至少 1 个(保证高可用)。 - 写入链路:商品变更通过 Canal 监听 MySQL Binlog → 发送至 MQ → 消费者增量更新 ES(保证最终一致性)。
- 查询链路:
- L1 本地缓存(Caffeine):缓存高热度搜索词(Top 1000 关键词)的返回结果,TTL 5min。
- L2 Redis 缓存:缓存次热词,TTL 1min。
- L3 ES 查询:使用
bool组合查询 +function_score按销量/评分自定义相关度。
- 性能优化:
- 禁用
*通配符查询,使用精确字段(keyword)聚合。 - 开启
routing路由,确保相同类目的商品落在同一分片,减少跨分片查询。 - 配置
refresh_interval调大(如 30s),降低写入压力。
- 禁用
- 索引拆分:按商品类目或时间分索引(