Java 大厂一面模拟:从线程本地存储到分库分表路由的连环拷问
开场说明
这是一场模拟 30 分钟左右的 Java 大厂一面,面向 1-3 年经验的 Java 后端候选人或校招高阶候选人。面试官风格贴近真实大厂一面节奏,注重基础原理、并发安全、JVM 底层、数据库设计与缓存一致性,同时结合典型业务场景(如订单、用户、活动系统)进行连续追问。整场面试强调"拷打感"------问题层层递进,从表象到原理,再到边界条件和线上落地取舍。
候选人画像:熟悉 Spring Boot、MySQL、Redis、Kafka,有中小型项目经验,但对并发模型、JVM 内存布局、分库分表路由策略等深度内容掌握不扎实。
面试强度:中高。主问题控制在 10 个以内,重点追问 4 个,覆盖 Java 基础、并发、JVM、MySQL、Redis、项目设计六大模块。
主问题部分
1. 你说用过 ThreadLocal,它解决了什么问题?在 Spring 事务管理中有何作用?
参考回答: ThreadLocal 用于实现线程本地变量存储,每个线程拥有独立的变量副本,避免共享状态带来的线程安全问题。在 Spring 事务中,TransactionSynchronizationManager 使用 ThreadLocal 存储当前事务的 Connection 或 TransactionStatus,确保同一线程内多次数据库操作共享同一个事务上下文。
追问方向预判: 内存泄漏风险、父子线程传递、Spring 事务传播如何依赖 ThreadLocal。
2. ThreadLocal 真的完全线程安全吗?什么情况下会导致内存泄漏?如何避免?
参考回答: ThreadLocal 本身不提供"线程间安全",而是提供"线程内隔离"。内存泄漏通常发生在线程池场景中:线程复用,Entry 的 key(ThreadLocal 实例)被弱引用回收,但 value 是强引用,若未调用 remove(),value 无法被 GC。
避免方式:
- 使用完务必调用
threadLocal.remove(); - 在 Filter 或 AOP 中统一清理;
- 避免在静态 ThreadLocal 中存储大对象。
追问方向预判: 弱引用 vs 强引用、ThreadLocalMap 结构、实际项目中的泄漏案例。
3. 你在项目里用过线程池吗?ThreadPoolExecutor 的核心参数有哪些?拒绝策略怎么选?
参考回答: 核心参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。
拒绝策略:
- AbortPolicy:抛异常,适合关键任务;
- CallerRunsPolicy:由调用线程执行,削峰填谷;
- DiscardPolicy/DiscardOldestPolicy:静默丢弃,慎用。
我们订单系统用 CallerRunsPolicy,防止突发流量压垮服务。
追问方向预判: 队列选型(ArrayBlockingQueue vs LinkedBlockingQueue)、动态调参、监控线程池状态。
4. 如果线程池任务抛异常了,你注意到过吗?怎么捕获和处理?
参考回答: 默认情况下,submit() 提交的任务异常会被 Future.get() 捕获,但 execute() 提交的任务异常会直接打印堆栈并终止线程。
处理方式:
- 重写 ThreadFactory 设置 UncaughtExceptionHandler;
- 使用 try-catch 包裹任务逻辑;
- 使用 CompletableFuture.exceptionally() 处理异步异常。
追问方向预判: 异常对线程池的影响、如何保证任务不丢失、是否影响其他任务。
5. 你说你们系统做了分库分表,路由策略是什么?怎么解决热点用户问题?
参考回答: 我们按 user_id 做 hash 取模分 8 库 × 8 表。路由逻辑封装在 ShardingSphere 中。
热点用户(如大 V 用户)会导致某几张表负载过高。解决方案:
- 对热点 user_id 做特殊映射,单独分配库表;
- 引入二级路由:先按 user_id 范围分,再按业务类型分;
- 缓存热点用户数据,减少 DB 压力。
追问方向预判: 路由算法一致性、扩容迁移成本、跨库查询如何办。
6. 分库分表后,怎么实现跨库分页查询?比如"查询某用户最近 100 条订单"?
参考回答: 不能直接 order by + limit。我们采用"归并排序 + 内存分页":
- 并行查询所有分片,获取各分片前 N 条;
- 在内存中归并排序,取全局前 100 条。
缺点:数据量大时内存压力大。优化方案:
- 限制分页深度(如最多查前 1000 条);
- 使用游标分页(cursor-based pagination);
- 将高频查询落到 ES。
追问方向预判: 性能瓶颈、一致性保障、是否支持跳页。
7. 你说用了 Redis 做缓存,怎么保证缓存和数据库的一致性?
参考回答: 我们采用"先更新 DB,再删除缓存"策略。配合延时双删:更新 DB 后,延迟 500ms 再删一次缓存,减少并发写导致的不一致窗口。
极端情况:删除缓存失败。解决方案:
- 使用消息队列异步重试删除;
- 设置缓存过期时间,兜底一致性;
- 关键数据走读写穿透模式(Cache-Aside → Read-Through)。
追问方向预判: 双删时机、消息可靠性、缓存击穿应对。
8. 缓存穿透和缓存雪崩你遇到过吗?怎么解决的?
参考回答:
- 缓存穿透:查询不存在的数据(如 user_id=-1)。解决方案:布隆过滤器 + 空值缓存。
- 缓存雪崩:大量 key 同时过期。解决方案:随机过期时间 + 多级缓存 + 热点数据永不过期。
我们活动系统在秒杀期间启用本地缓存(Caffeine)作为一级缓存,Redis 为二级,DB 为三级。
追问方向预判: 布隆过滤器误判率、本地缓存一致性、如何识别热点 key。
9. 你们用 Kafka 做异步解耦,怎么保证消息不丢失?
参考回答:
- 生产者:启用 acks=all,重试机制,本地落盘兜底;
- Broker:min.insync.replicas ≥ 2,避免单副本写入;
- 消费者:手动提交 offset,消费成功后再提交,配合幂等处理。
我们订单创建后发消息到 Kafka,下游扣减库存,通过唯一订单 ID 做幂等。
追问方向预判: 消息重复消费、事务消息、延迟消息实现。
10. 如果 Kafka 消费者挂了,重启后怎么保证不丢消息?
参考回答: Kafka 消息持久化在磁盘,消费者通过 offset 记录消费位置。重启后从上次提交的 offset 继续消费。
关键点:
- 关闭自动提交(enable.auto.commit=false);
- 业务处理成功后再手动提交 offset;
- 监控 lag,及时发现消费延迟。
追问方向预判: offset 管理、再平衡影响、如何快速恢复。
追问部分(重点拷打)
追问 1:ThreadLocal 的内存泄漏,你说要 remove,但如果任务异常了没执行到 remove 怎么办?
期望回答: 必须在 finally 块中调用 remove(),或使用 try-with-resources 模式封装 ThreadLocal。更优方案是在框架层统一处理,如 Spring 的 RequestContextHolder 在请求结束时自动清理。
深入点: 线程池 + 异常 = 内存泄漏高危场景,必须强制规范。
追问 2:分库分表路由用 hash 取模,如果要从 8 库扩容到 16 库,怎么迁移数据?业务不停机?
期望回答:
- 采用一致性哈希或虚拟桶(如 ShardingSphere 的柔性事务);
- 双写方案:新旧路由同时写,读新路由,逐步迁移旧数据;
- 使用数据同步工具(如 Canal + Kafka)实时同步;
- 迁移完成后切流量,下线旧库。
深入点: 扩容成本高,设计初期应预留足够分片数(如 1024 虚拟分片)。
追问 3:你说"先更新 DB 再删缓存",但如果删缓存成功,但下一秒另一个线程读了旧数据并回填缓存,怎么办?
期望回答: 这是经典"并发写导致缓存脏数据"问题。解决方案:
- 加分布式锁(如 Redisson),更新 DB 期间禁止读缓存;
- 使用版本号或时间戳,缓存回填时校验数据版本;
- 接受短暂不一致,依赖过期时间兜底。
深入点: 强一致 vs 性能,大厂通常选择"最终一致 + 监控告警"。
追问 4:Kafka 消费者组 rebalance 时,正在处理的消息会中断吗?怎么保证不重复不丢失?
期望回答: Rebalance 时,消费者会暂停 poll,触发 onPartitionsRevoked 回调。此时应:
- 完成当前批次消息处理;
- 手动提交 offset;
- 在 onPartitionsAssigned 中重新绑定处理逻辑。
配合幂等消费(如数据库唯一索引、Redis setnx),可避免重复。
深入点: 消费逻辑必须无状态或状态可恢复,避免 rebalance 导致数据错乱。
面试点评
本场面试主要考察候选人在高并发、分布式环境下的技术深度与系统思维。重点卡点在于:
- ThreadLocal 的内存泄漏机制:多数候选人知道要 remove,但说不清弱引用与内存泄漏的关系;
- 分库分表的路由与扩容:能说出 hash 取模,但无法回答平滑扩容方案;
- 缓存一致性边界:知道"先更新 DB 再删缓存",但无法处理并发回填问题;
- 消息队列的可靠性保障:能答出手动提交 offset,但忽略 rebalance 对消费的影响。
候选人需加强"原理 → 边界 → 线上问题 → 取舍落地"的完整链路思考,避免只背八股文。
技术补丁包
-
ThreadLocal 内存泄漏机制 原理:ThreadLocalMap 的 Entry 使用弱引用 key(ThreadLocal 实例),value 为强引用。线程池复用线程时,若未 remove,value 无法被 GC。 设计动机:实现线程内变量隔离,避免全局共享。 边界条件:线程池 + 异常未清理 = 内存泄漏;静态 ThreadLocal 存储大对象风险高。 落地建议:在 Filter 或 AOP 中统一调用 remove(),或使用 try-finally 包裹。
-
ThreadPoolExecutor 拒绝策略选型 原理:当队列满且线程数达 maximumPoolSize 时触发拒绝策略。 设计动机:防止系统过载,提供可控的流量整形。 边界条件:AbortPolicy 适合核心业务;CallerRunsPolicy 可削峰但可能阻塞调用方。 落地建议:订单、支付类用 CallerRunsPolicy;日志、统计类用 DiscardPolicy。
-
分库分表 hash 路由与扩容 原理:user_id % N 决定数据所在库表。 设计动机:分散数据压力,提升并发能力。 边界条件:扩容需数据迁移,hash 取模导致大量数据重分布。 落地建议:初期使用虚拟分片(如 1024 分片),扩容时仅调整映射关系;或采用一致性哈希。
-
跨库分页查询实现 原理:各分片独立查询后归并排序。 设计动机:支持全局有序查询。 边界条件:数据量大时内存 OOM;不支持高效跳页。 落地建议:限制分页深度,使用游标分页,或引入 ES 做搜索。
-
缓存一致性:先更新 DB 再删缓存 原理:保证 DB 为最新数据源,缓存为辅助。 设计动机:平衡性能与一致性。 边界条件:并发写可能导致缓存回填旧数据。 落地建议:结合延时双删、分布式锁或版本号校验。
-
Redis 缓存穿透解决方案 原理:查询不存在的数据,绕过缓存直击 DB。 设计动机:防止恶意攻击或误查询压垮数据库。 边界条件:布隆过滤器有误判率;空值缓存可能被恶意刷。 落地建议:布隆过滤器 + 空值缓存(短 TTL),关键业务加验证码或限流。
-
Kafka 消息可靠性保障 原理:生产者 acks=all,Broker 多副本,消费者手动提交 offset。 设计动机:确保消息不丢失、不重复。 边界条件:网络分区可能导致 ISR 收缩;消费者 rebalance 可能重复消费。 落地建议:启用幂等生产者和事务消息,消费端做唯一键去重。
-
Spring 事务与 ThreadLocal 绑定 原理:TransactionSynchronizationManager 使用 ThreadLocal 存储事务资源。 设计动机:实现编程式事务与声明式事务的统一管理。 边界条件:异步方法中事务失效,因 ThreadLocal 不跨线程。 落地建议:异步事务需显式传递事务上下文,或使用 TransactionTemplate。
-
JVM 堆外内存泄漏排查 原理:DirectByteBuffer、JNI、ThreadLocal 等使用堆外内存,不受 GC 管理。 设计动机:提升 I/O 性能,减少 GC 压力。 边界条件:未释放 DirectByteBuffer 导致 Native Memory 耗尽。 落地建议:使用 NMT(Native Memory Tracking)监控,合理设置 -XX:MaxDirectMemorySize。
-
MySQL 主从延迟对缓存一致性的影响 原理:读写分离下,从库读取可能落后主库。 设计动机:提升读性能。 边界条件:刚写入的数据从从库读不到,导致缓存回填旧值。 落地建议:关键读走主库,或使用 GTID 判断从库同步状态。
-
Redisson 分布式锁的实现原理 原理:基于 Redis 的 SET resource_name unique_value NX PX timeout 实现。 设计动机:实现跨 JVM 的互斥访问。 边界条件:锁过期但业务未完成,导致其他线程误获锁。 落地建议:使用看门狗机制自动续期,或业务逻辑设计为幂等。
-
CompletableFuture 异常处理 原理:异步任务异常不会抛出,需通过 get() 或 exceptionally() 捕获。 设计动机:支持非阻塞异步编程。 边界条件:未处理异常导致任务静默失败。 落地建议:始终添加 exceptionally() 或 whenComplete() 处理异常。
-
ShardingSphere 柔性事务支持 原理:提供 XA、SEATA、本地消息表等分布式事务方案。 设计动机:解决分库分表下的数据一致性。 边界条件:XA 性能差,SEATA 需额外中间件。 落地建议:非核心业务用最终一致,核心业务用 TCC 或 Saga。
-
G1 GC 的 Region 设计与停顿预测 原理:将堆划分为多个 Region,按回收价值排序,优先回收垃圾多的 Region。 设计动机:实现可预测的停顿时间。 边界条件:Mixed GC 阶段可能因存活对象多而停顿过长。 落地建议:合理设置 -XX:MaxGCPauseMillis,监控 Evacuation Pause。
-
本地缓存与 Redis 多级缓存一致性 原理:Caffeine 做一级缓存,Redis 做二级缓存。 设计动机:减少网络开销,提升响应速度。 边界条件:本地缓存无法感知其他节点更新。 落地建议:通过消息通知失效本地缓存,或设置短 TTL。
-
Kafka Consumer Rebalance 机制 原理:消费者组内成员变化时重新分配分区。 设计动机:实现负载均衡与故障转移。 边界条件:rebalance 期间消费暂停,可能导致重复或丢失。 落地建议:在 onPartitionsRevoked 中提交 offset,消费逻辑做幂等。
-
MySQL 间隙锁防止幻读 原理:在可重复读隔离级别下,对范围查询加间隙锁,阻止其他事务插入。 设计动机:保证事务内多次查询结果一致。 边界条件:范围查询加锁范围大,易导致死锁。 落地建议:避免大范围查询,使用唯一索引缩小锁范围。
-
Spring AOP 代理机制与循环依赖 原理:默认使用 JDK 动态代理(接口)或 CGLIB(类)。 设计动机:实现声明式编程。 边界条件:同类方法调用不走代理,导致 @Transactional 失效。 落地建议:通过 ApplicationContext 获取代理对象,或使用 AspectJ 编译时织入。
-
Redis 持久化 AOF 与 RDB 取舍 原理:AOF 记录写命令,RDB 保存快照。 设计动机:AOF 数据更安全,RDB 恢复更快。 边界条件:AOF always 刷盘性能差,everysec 可能丢 1 秒数据。 落地建议:生产环境用 AOF everysec + RDB 定时备份。
-
分布式系统最终一致性保障 原理:通过消息队列、对账、补偿机制实现数据最终一致。 设计动机:在性能与一致性之间取得平衡。 边界条件:网络分区、消息丢失、消费失败。 落地建议:关键业务加对账系统,定时扫描不一致数据并修复。