经纬恒润 Java 秋招一面:Kafka、线程池与线上问题排查
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- ConcurrentHashMap 的 get 方法加锁吗?
- 联合索引怎么建立的?
- Kafka 怎么保证可靠性、不重复消费?
- rebalance 什么原因导致的?最常见的?
- 线程池参数怎么设置的?拒绝策略使用的什么?Kafka 适合什么拒绝策略?
- 项目里 MCP 怎么用的,用户输入到输出整个链路流程
- Kafka 消息堆积怎么排查?
- 增加消费者组,消费 TPS 无明显提升,什么原因?
- 接口偶发高 RT 怎么排查?
- 水平越权出现在生产环境,但测试环境无法复现,怎么办?
- 多线程状态变更怎么保证不错乱?
- 为什么红黑树转换节点是 8?
反问
《参考解析》
-
ConcurrentHashMap 的 get 为什么可以不加锁:它的读路径靠 volatile 语义保证可见性——节点数组元素与 Node 的 val/next 都是 volatile 的,读线程拿到的要么是旧值要么是新值,不会读到”写了一半”的对象。所以 get 不加锁,锁只出现在写路径(CAS 或 synchronized 锁桶头)。追问通常接扩容:读遇到 ForwardingNode 时会去新表里找,这是为了扩容期间读也不阻塞。答题时把”读不加锁、写才加锁、扩容可并行”三层说全。
-
联合索引与最左前缀:建索引要先看查询模式——把等值条件里区分度高的列放左边,范围条件列放最后,因为范围之后的列用不上索引的有序性。要能顺手画出一个 (a,b,c) 索引支持的查询清单、指出
where b=?这类为什么用不上。另外注意索引不是越多越好:每个索引都要在写路径上付出维护代价,并且要考虑覆盖索引能否免掉回表。 -
Kafka 的可靠与去重是两件事,分开答:可靠性一端看生产侧(acks 配置、重试、副本数与最小同步副本数)和存储侧(副本机制、刷盘策略),消费侧看是否手动提交位点、先处理完再提交。不重复消费本质上是”端到端幂等”问题,Kafka 自己做不到,要靠生产端幂等生产者加消费端业务幂等(唯一键、去重表、状态机加版本号)。面试官最想听的是你承认”至少一次投递下必然可能重复”,然后给出幂等方案。
-
rebalance 的常见触发原因:消费者加入或离开(扩缩容、进程重启)、心跳超时被判掉线、单批处理时间超过最大轮询间隔被踢出组、订阅的 topic 分区数变化。生产上最常见的其实是”业务处理太慢导致超过最大轮询间隔”和”消费者实例上下线”。答完可以补一句治理手段:调大合理的超时、控制单批条数、用静态成员或者协作式再平衡减少抖动。
-
线程池参数与拒绝策略,关键是给出推导过程:核心线程数、最大线程数、队列、空闲回收、拒绝策略五项要说清彼此关系——队列一长,最大线程数其实很难起作用。业务上分两类:CPU 密集按核数靠上设,IO 密集按”等待时间与计算时间的比值”放大。拒绝策略里,对”消息必须处理”的链路,让调用线程自己执行(CallerRunsPolicy)能形成反压,比直接丢弃或抛异常的 AbortPolicy 更合适;丢弃类策略在核心链路上一般不能接受,除非业务明确允许丢。
-
消息堆积怎么排查:先量化,看消费延迟(lag)是在涨、在平还是在跌,以及生产速率和消费速率的对比,判断是生产暴涨还是消费变慢。消费变慢再往下拆:单条处理耗时是否变长(下游依赖、慢 SQL、外部接口超时)、是否发生了 rebalance 抖动、分区是否有热点导致部分消费者空转、消费者实例是否掉过线。扩容分区和消费者是解法之一,但得先确认瓶颈不在下游。
-
加了消费者 TPS 不涨,通常是这几处卡住:消费者实例数超过了订阅的分区数,多出来的实例分不到分区、纯属空转;分区键设计导致数据倾斜,热点分区把个别消费者压满;瓶颈根本不在消费侧,而在下游数据库或第三方接口;单批拉取条数太小、或者每条都同步提交位点,把时间花在了往返上。答题时把”并行度上限=分区数”这条先说清,是最容易拿分的一句。
-
接口偶发高 RT 与”只在生产复现”的越权问题,都是排查方法论题:偶发高 RT 先看分布而不是均值——P99 出现毛刺时依次怀疑 GC 停顿或内存压力、连接池排队、锁竞争、慢 SQL、线程池排队、下游超时与网络抖动,靠链路追踪定位到具体环节再谈优化。水平越权只在生产复现,往往说明差异在数据或配置:生产有跨租户数据、网关或鉴权中间件的配置不同、测试环境缺真实角色。正确做法是从代码和鉴权链路上审出越权点,补上统一的服务端校验与审计日志,而不是靠”复现不出来”结案。
-
红黑树阈值 8 与多线程状态:阈值 8 是统计学结论——哈希值均匀分布时,单个桶里的节点数近似服从泊松分布,长度到 8 的概率已经低到千万分之几,所以正常情况下根本用不到红黑树,设成 8 是为了让极端情况有兜底、又不必频繁树化;退化回链表的阈值 6 留了缓冲区,避免在 7 附近反复转换。多线程状态变更不错乱,本质是原子性、可见性、有序性三件事:把状态变更收敛到一个入口、用锁或 CAS 保证复合操作的原子性、用 volatile 或锁保证可见性,极端场景可以直接用状态机加版本号做乐观校验。