面灵AI→

字节跳动3年经验Java社招面经:一面六方向深挖三面OC

轮次
一面
结果
OC
时间
2026-09
来源
牛客网

《面试题目》

一面:并发编程

  1. ConcurrentHashMap 在 JDK 7 和 JDK 8 中的实现有什么区别?
  2. ConcurrentHashMap 的 size() 方法是准确的吗?
  3. ThreadLocal 在线程池场景下会有什么问题?

一面:JVM 4. JVM 线上 Full GC 频繁,怎么排查? 5. GC 耗时 10 毫秒,为什么系统却卡顿了 10 秒?

一面:MySQL 6. MySQL 的 MVCC 和间隙锁是怎么配合防止幻读的?

一面:Redis 7. Redis 的缓存穿透、击穿、雪崩分别是什么,怎么解决?

一面:Spring 8. Spring 循环依赖的三级缓存是怎么解决的?

一面:消息队列 9. 消息队列如何保证消息不丢失?

二面 10. 系统设计:设计一个支持亿级流量的电商大促系统。 11. 分布式事务选型:TCC、SAGA 与消息最终一致性分别适合什么场景,空回滚和悬挂怎么解?

三面(交叉面) 12. 系统设计:设计一个全链路压测平台。

《参考解析》

ConcurrentHashMap 的演进

JDK 7 用分段锁:整个表切成若干 Segment,每段一把可重入锁,并发度上限就是段数且不能扩容。JDK 8 改成 CAS + synchronized:数组空时 CAS 建表,桶空时 CAS 写入,桶非空时 synchronized 锁住头节点再操作链表或红黑树,并发度随数组扩容而提升。size() 在 JDK 8 里是近似值,靠 baseCount 加一组 CounterCell 分散热点求和,思路同 LongAdder,统计期间有并发修改就只能是估计。

ThreadLocal 在线程池里的泄漏

key 是弱引用、value 是强引用。线程池的线程长期存活,ThreadLocal 被回收后 Entry 变成 key=null 而 value 仍在,这条无效条目只能等 ThreadLocalMap 在扩容或清理时顺带处理,数量一多就 OOM。用 finally 里的 remove() 兜底,别只依赖弱引用。

Full GC 频繁的排查链路与「GC 只停 10ms 系统卡 10 秒」

先 jstat 看各代回收频率和老年代占用趋势,再 jmap dump 堆、用 MAT 的 Dominator Tree 与 Leak Suspects 定位大对象持有者,回到代码修引用。至于停顿短但卡顿长,说明瓶颈不在 STW 本身,而在 GC 之后的连锁反应:本地缓存被整体回收导致后续请求全部穿透到数据库,或停顿期间任务在线程池里堆积、恢复瞬间打满下游。止损要先限流、再补缓存预热。

MVCC 与间隙锁防幻读

可重复读下快照读走 MVCC:事务第一次读时生成 Read View,之后按可见性规则读历史版本,同一事务内两次普通 SELECT 结果一致。当前读(SELECT … FOR UPDATE、UPDATE、DELETE)走间隙锁加临键锁,锁住记录本身以及记录之间的间隙,阻止其他事务插入新行,从而挡住幻读。间隙锁只在可重复读级别存在,读已提交没有。

缓存穿透、击穿、雪崩

穿透是查根本不存在的数据,绕过缓存直接打库,用布隆过滤器拦掉不可能存在的 key,并缓存空值(过期时间要短)。击穿是单个热点 key 过期瞬间大量请求涌向数据库,用互斥锁只放一个线程去回源,或干脆给热点 key 逻辑过期、由后台异步续期。雪崩是大量 key 同一时刻失效,给过期时间加随机抖动、做多级缓存,并配合限流降级。

Spring 三级缓存为什么必须是三层

singletonObjects 放完整 Bean,earlySingletonObjects 放半成品,singletonFactories 放 ObjectFactory。A 实例化后先把工厂放进三级缓存,填充属性时发现依赖 B,创建 B 又回头依赖 A,此时从三级缓存取工厂生成 A 的早期引用。之所以不能省掉第三层直接放半成品,是因为存在 AOP 时最终要注入的是代理对象,只有把「生成早期引用」这件事延后成工厂调用,才能在需要时提前触发代理创建,同时保证同一 Bean 只生成一次。

消息队列不丢消息的三个环节

生产端要确认:同步发送或带回调的异步发送,失败重试,配合本地消息表保证「业务成功必发出」。Broker 端要持久化:同步刷盘不丢,异步刷盘在宕机时会丢;再叠主从复制与刷盘策略。消费端要手动 ACK:关掉自动提交位点,业务处理成功后再提交,失败走重试与死信队列,业务侧再做幂等。

亿级流量大促的分层设计

自上而下:CDN 承接静态资源与图片,网关层限流(令牌桶或漏桶)与熔断,接入层做无状态水平扩容。核心链路用 Redis 预扣库存,Lua 脚本把「查库存 + 校验一人一单 + 扣减」合成一次原子操作,成功后发消息异步落库,MQ 起到削峰填谷的作用,消费者按自身能力拉取。要做到单点可预期:算清楚 Redis 单节点 10 万级 QPS,靠分片扩到百万级;再往上就在客户端做本地缓存和排队页。数据一致性用预扣加确认、定时对账与告警兜住。

分布式事务选型

TCC 适合资金这类核心链路,Try 阶段预留资源、Confirm 提交、Cancel 释放,一致性最强但业务侵入大,必须自己处理空回滚(Cancel 先于 Try 到达时不能误释放)与悬挂(空回滚之后迟到的 Try 必须丢弃),常见做法是事务控制表加状态机。SAGA 把长事务拆成一串本地事务,每步配一个补偿动作,适合流程长、能接受中间态的业务。消息最终一致性适合非核心的异步场景,靠本地消息表加定时补偿做到最终一致,对业务侵入最小但有时延。

全链路压测平台的关键是流量染色

压测流量在入口打上标识,沿调用链一路透传,网关、RPC、消息、缓存、数据库各层都按标识路由到影子资源:数据写影子库影子表,不与生产数据混;服务按分组路由到压测实例;监控指标单独统计,避免污染生产大盘。再补齐数据构造与清理、压测开关、以及压测结束后的数据回收,才算能反复用。