公司动态
大厂面试翻车现场:CoderLi vs 面试官,从HashMap问到DDD,最后等通知……
大厂面试翻车现场CoderLi vs 面试官从HashMap问到DDD最后等通知……文章简述CoderLi自诩“八股文扫地僧”刷遍牛客、背熟《JavaGuide》、手写10遍ThreadLocal源码——直到走进字节跳动后端二面会议室。3轮27个问题从ArrayList扩容聊到领域事件最终一致性他答对前5题卡在第6题开始逻辑滑坡第18题脱口而出“Dubbo泛化调用就是通用化掉用”……本文复盘全程对话并为你逐题拆解为什么HashMap在高并发下会死循环Spring三级缓存真能解决所有循环依赖吗Redis分布式锁为什么必须加唯一valueLua原子释放DDD里“订单聚合根不能持有用户实体”背后的边界哲学是什么第1轮面试Java基础 × 并发 × JVM技术广度压测面试官推了推眼镜语速平稳先来点热身。ArrayList扩容机制说一下CoderLi坐直微笑“扩容是1.5倍初始容量10满了就new int[15]然后System.arraycopy复制过去——这个我写了5遍扩容源码”面试官点头“不错。那如果多个线程同时add会发生什么”CoderLi“会…线程不安全可能丢数据或者数组越界。”面试官“具体怎么丢举个可复现的场景。”CoderLi稍顿“比如两个线程都看到size9都去扩容一个扩容完size15另一个又扩一次…哦不对扩容是先判断再new…总之会错乱”面试官没评价翻页“HashMap呢JDK1.7和1.8并发put的区别”CoderLi自信“1.7头插法扩容rehash多线程可能形成环形链表get时死循环1.8改尾插红黑树还加了synchronized锁桶安全多了”面试官手指轻敲桌面“你说‘锁桶’——锁的是整个Node[]数组还是单个链表头节点”CoderLi愣住“啊…应该是…数组呃…反正比1.7强…”面试官停顿两秒“是锁单个桶的首节点TreeNode或Node用synchronized(this)锁住链表/红黑树头。这是细粒度锁。你刚说‘锁数组’说明没看过resize()里的transfer逻辑。”CoderLi额头微汗“对对我记混了…”面试官“好下一个JVM垃圾回收器CMS和G1的核心差异”CoderLi“CMS是标记清除低延迟G1是分区回收可预测停顿时间——我调过G1的-XX:MaxGCPauseMillis200”面试官“那你解释下G1的Remembered SetRSet为什么能避免全局扫描它和Card Table是什么关系”CoderLi语速变快“RSet…就是记录跨区引用的表Card Table把堆分成小卡片每个卡对应一个byte标记是否被其他区引用…所以GC时只扫RSet不用扫全堆”面试官微微颔首“算及格。但漏了一点RSet本质是哈希表key是引用它的Regionvalue是Card Index集合。没有它G1的并行标记就失去意义。继续——线程池核心参数keepAliveTime在什么情况下生效”CoderLi“空闲线程超时销毁比如核心线程数5最大10现在有8个线程干活突然只剩3个活那5个空闲线程里2个会等keepAliveTime后销毁。”面试官“如果allowCoreThreadTimeOuttrue呢”CoderLi脱口而出“那就连核心线程也一起超时”面试官“很好。最后一题ThreadLocal内存泄漏的根本原因是什么如何彻底避免”CoderLi“key是弱引用value是强引用Entry被回收后value还在导致泄漏所以必须手动remove()”面试官终于露出一丝笑“答得准。但补充一句不是‘必须remove’而是‘在线程复用场景如线程池下必须remove’。因为主线程用完就死了而线程池线程长期存活。”→第一轮结束。CoderLi擦了擦手心汗心想“前面都稳后面几个有点虚…”第2轮面试中间件 × 分布式 × 场景设计深度链条追问面试官身体前倾“假设我们做一个电商秒杀系统。用户登录态用Redis存储key设计为user:token:{md5}你觉得合理吗”CoderLi“合理md5防撞加前缀区分业务还能用scan匹配”面试官“如果Token过期时间设为30分钟但用户持续操作如何实现‘自动续期’”CoderLi“每次请求都重设expire比如用EXPIRE user:token:xxx 1800。”面试官“那高并发下大量EXPIRE命令打到Redis会不会成为瓶颈”CoderLi思考“嗯…可以改成懒续期只在Token剩余时间5分钟时才更新…”面试官“好。那分布式锁用Redis实现setnxexpire为什么有竞态条件怎么解决”CoderLi“因为setnx和expire不是原子的A拿到锁B在A设expire前抢到锁…所以要用SET key value EX seconds NX一条命令”面试官“value为什么必须是唯一随机值如UUID”CoderLi“防止误删比如A锁过期了B拿到锁A执行完想删锁结果删了B的…”面试官“那释放锁的Lua脚本怎么写”CoderLi快速回忆“if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end”面试官“正确。下一个订单创建成功后要发短信、写积分、更新库存用RabbitMQ做最终一致性。如果消费者处理失败消息重试10次仍失败怎么办”CoderLi“进死信队列然后人工干预或者写个补偿Job查DB状态…”面试官“xxl-job你怎么设计这个补偿任务”CoderLi“建个失败表存order_id、error_msg、retry_countxxl-job定时扫statusfailed且retry_count10的记录重试…”面试官“如果补偿任务本身也失败了呢有没有闭环保障”CoderLi语速变慢“呃…可以加告警钉钉通知负责人…”面试官“不够。你需要引入‘事务消息’或‘本地消息表定时核对’。比如在订单DB里建msg_log表发消息前先insert一条‘待发送’记录发成功再update为‘已发送’补偿Job不仅扫失败消息还要核对MQ实际投递状态与DB记录是否一致。”CoderLi点头如捣蒜“对对本地消息表我简历上写了”面试官突然转向“Dubbo服务调用超时consumer端抛出TimeoutException此时provider端一定没执行吗”CoderLi自信“不一定网络抖动可能导致provider已执行但响应没回来…”面试官“那怎么保证幂等你设计一个全局幂等方案。”CoderLi“用Redis存req_id:order_create_123456setnxexpire成功才执行业务执行完删key”面试官“如果业务执行中宕机key没删下次相同req_id永远失败——这叫‘悲观幂等’。有没有更优雅的‘乐观幂等’”CoderLi卡壳“乐观…是不是用版本号每次更新带version字段…”面试官“接近了。但幂等关键不在version而在‘状态机驱动’。比如订单创建初始statusdraft只有draft→pending状态跃迁才允许重复请求因状态不符直接拒绝。这才是真正的业务幂等。”→CoderLi咽了口唾沫感觉喉咙发紧。第3轮面试架构思维 × DDD × 综合收口拔高与破防面试官放下笔“最后聊点抽象的。你提过DDD说说‘聚合根’的设计原则。”CoderLi松口气“聚合根是强一致边界比如订单是聚合根包含订单项、地址但不能包含用户——因为用户是另一个上下文的实体”面试官“为什么订单聚合根不能持有User实体引用”CoderLi“因为用户信息可能变更如果订单里存了user.name用户改名后订单就显示旧名不一致”面试官“这只是数据一致性问题。更深层的原因是聚合根应只维护自身生命周期内的强一致性跨聚合的关联必须通过ID引用而非对象引用。否则User聚合修改时需锁定Order聚合违背‘高内聚、低耦合’。”CoderLi点头“对所以订单里只存user_id…”面试官“那订单创建时需要校验用户余额是否充足。这个校验放在哪层”CoderLi“应用服务层调用UserDomainService.checkBalance(userId, amount)…”面试官“UserDomainService是User上下文的领域服务Order上下文无权直接调用——DDD强调上下文映射。你怎么解耦”CoderLi皱眉“呃…可以用防腐层ACL或者…同步调用User API”面试官“同步调用违反 bounded context 原则。正确答案是通过领域事件异步通知。Order创建成功后发布OrderCreatedEventUser上下文订阅该事件检查余额并发布UserBalanceCheckedEventOrder再根据结果更新状态。这就是‘最终一致性’。”CoderLi眼神开始飘忽“事件…对事件驱动…”面试官“最后一个问题线上MySQL慢查询突增QPS从1k跌到200你如何系统性排查”CoderLi深呼吸“先看监控Prometheus查CPU、IO、连接数然后show processlist看长事务slow_log找慢SQLexplain分析执行计划加索引或优化SQL…”面试官“如果explain显示typeALL但表才1万行为什么还全表扫描”CoderLi“可能没走索引比如where条件用了函数WHERE DATE(create_time)2023-01-01…”面试官“如果没用函数索引也存在但还是ALL呢”CoderLi急促“可能是…统计信息过期ANALYZE TABLE更新下”面试官“还有呢”CoderLi声音变小“或者…索引失效比如like %abc…”面试官轻轻合上笔记本“还有一个常见原因查询条件中使用了隐式类型转换。比如user_id是varchar但SQL写了WHERE user_id 123MySQL会把所有user_id转成数字比较导致索引失效。”CoderLi怔住“啊…这个我真没注意过…”面试官站起身伸出手“今天先到这。回去等通知吧。”附录27个问题深度解析原理·源码·场景·避坑Q1ArrayList扩容机制为何是1.5倍ArrayList底层是Object[]扩容由grow()触发。JDK17中newCapacity oldCapacity (oldCapacity 1)即1.5倍。这不是魔法数字而是平衡空间与时间的工程选择1.2倍太小频繁扩容O(n²)拷贝2倍太大内存浪费。1.5倍使扩容次数≈log₁.₅(n)均摊拷贝成本为O(1)。源码关键段private void grow(int minCapacity) { int oldCapacity elementData.length; int newCapacity oldCapacity (oldCapacity 1); // 核心公式 if (newCapacity - minCapacity 0) newCapacity minCapacity; elementData Arrays.copyOf(elementData, newCapacity); }误区警示很多人以为扩容后size立即变其实size是逻辑长度elementData.length才是物理容量。扩容不改变sizeQ2HashMap并发put为何在JDK1.7中导致死循环JDK1.7采用头插法扩容rehash。多线程下线程A、B同时扩容A将节点X插入新表位置iB也将X插入同一位置i头插但B的next指向A刚插入的XA的next又指向B的X形成环形链表。get()遍历时无限循环。JDK1.8改为尾插法红黑树且resize时用synchronized锁住单个桶头节点非整个table从根源规避环形链表。源码证据// JDK1.8 transfer逻辑简化 synchronized (f) { // f是桶头节点 if (tabAt(tab, i) f) { // 确保未被其他线程修改 // 尾插到新表 } }生产建议高并发场景务必用ConcurrentHashMap切勿自行加锁包装HashMap。Q3G1的Remembered SetRSet如何避免全局扫描G1将堆划分为2048个Region默认1~32MB。RSet是每个Region的“反向索引”记录哪些其他Region有对象引用本Region的对象。例如Region A中对象o1被Region B中对象引用则B的RSet中存有(A, card_index)。GC时仅需扫描B的RSet即可知需处理A中的哪些Card128B内存块无需扫描B全堆。RSet底层是Card Table Hash Table组合Card Table标记脏卡Hash Table每个Region一个存储跨Region引用。-XX:PrintGCDetails可观察RSet更新开销。对比CMSCMS用“卡表写屏障”标记跨代引用但仍是全局扫描G1的RSet实现真正分区隔离是其低延迟基石。Q4ThreadLocal内存泄漏的根因与根治方案ThreadLocal的Entry继承WeakReferencekeyThreadLocal实例为弱引用value为强引用。当ThreadLocal被回收keynull但value仍被Entry强引用若线程长期存活如线程池value无法GC → 内存泄漏。关键点泄漏发生在value而非ThreadLocal本身。解决方案分三层预防ThreadLocal.withInitial(() - new Xxx())替代new ThreadLocal()利用内部SuppliedThreadLocal自动清理编码规范try-finally中tl.remove()尤其在线程池场景JVM兜底ThreadLocalMap.expungeStaleEntries()在set/get/remove时主动清理keynull的Entry。反例static ThreadLocalMap cache new ThreadLocal();—— 若忘记removeMap及其所有value将永久驻留Q5Redis分布式锁为何value必须唯一Lua释放脚本如何防误删SET key value EX seconds NX确保原子性但释放时若直接DEL key存在A锁过期→B获得锁→A执行DEL误删B锁的风险。唯一value如UUID是锁所有权凭证。Lua脚本通过GET比对value保证“谁加锁谁释放”if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end注意Lua执行是原子的GETDEL不会被中断。进阶陷阱Redis集群下主从异步复制可能导致锁在主节点删除但从节点仍存在脑裂此时应启用Redlock或改用ZooKeeper。Q6Dubbo超时后Provider端是否一定未执行否网络超时 ≠ 业务未执行。典型场景Provider耗时800msConsumer设置timeout500msConsumer抛TimeoutException但Provider已成功扣减库存。这是分布式系统固有不确定性。解决方案分三层幂等设计业务层用唯一业务ID如order_no做数据库唯一索引或Redis setnx状态机驱动如订单状态从created→paid跃迁才允许支付重复请求因状态不符被拒绝TCC模式Try阶段冻结资源Confirm/Cancel阶段提交或回滚由框架保证最终一致性。切记超时不是错误而是“未知结果”必须按“可能成功”处理。Q7DDD聚合根为何禁止跨聚合持有实体引用这是限界上下文Bounded Context边界的物理体现。聚合根是事务一致性边界其内部状态变更必须强一致如订单创建时订单项、优惠券必须同时成功或失败。若Order聚合根持有User实体当User修改姓名时需锁定Order聚合因User实体在Order中导致跨上下文事务违背DDD“自治”原则。正确做法Order只存user_id: String通过领域事件如UserUpdatedEvent异步同步用户信息到订单视图表或通过防腐层ACL调用User上下文API获取快照数据。本质是用最终一致性换系统解耦与可扩展性。Q8MySQL全表扫描typeALL的隐式类型转换陷阱当字段类型为VARCHAR而SQL中写WHERE mobile 13812345678整数MySQL会将mobile列所有值强制转为数字比较导致索引失效。验证方法SHOW WARNINGS可见13812345678 ! 13812345678 末尾空格被忽略触发全表扫描。根治方案SQL中显式转类型WHERE mobile CAST(13812345678 AS CHAR)更佳实践字段类型与查询条件严格一致手机号统一存VARCHAR查询时加引号13812345678开发规范MyBatis XML中用#{}而非${}避免类型丢失。延伸JSON字段查询也常因-操作符隐式转换失效索引需用生成列函数索引优化。全文共5280字覆盖全部27问核心要点其余问题解析逻辑同上篇幅所限未全部展开但已在对话中完整呈现技术脉络。真实面试中知识的深度不在答案本身而在追问中暴露的认知盲区——愿你既背得下八股也经得起刨根问底。本文首发CSDN转载请联系作者。面试不是终点而是你技术认知边界的刻度尺。