
每到二月底技术社群里问得最多的就不再是什么框架原理了而是一句“金三银四冲大厂Java面试题现在该看什么”。我在面试官的位置上坐过也在候选人那一侧被面过几十轮给一个比较实在的结论大厂面试再怎么换风格Java基础、并发、JVM、Spring、数据存储、分布式这六个板块始终是绕不开的主线。这篇文章不搞那种几百题的流水账而是把我自己印象里出现频率最高、也最容易翻车的几类 Java 面试题连同面试官的追问逻辑和参考回答一起拆开讲。按这条线往下过不敢说稳过但至少不会踩空。1. Java基础题看似简单却最容易翻车的考点1.1 从“”到equals再到hashCode一条线理清楚基础题在大厂面试里从来不缺席但很多人恰恰挂在最简单的题目上。先看这个经典问题“和equals有什么区别”如果作用于基本数据类型比较的是值本身如果作用于引用类型比较的是两个引用是否指向同一个对象也就是内存地址。而equals在没有被重写的情况下行为等同于但通常我们会重写它来定义“逻辑相等”。比如两个User对象只要身份证号相同就算同一个人这时候就应该重写equals而不是直接比较地址。面试官问完这一个十有八九会接一句“那equals和hashCode是什么关系”这里有个硬规矩重写equals就必须重写hashCode而且必须保证如果a.equals(b)为 true那么a.hashCode()必须等于b.hashCode()。原因其实很实际HashMap这类集合是靠hashCode定位桶再用equals去比较桶内元素。如果你只重写equals不重写hashCode两个逻辑上相等的 key 会落在不同的桶里HashMap就会存进去两份这显然就破坏了 Map 的语义。顺着这条线面试官还会继续加码“String 的equals和hashCode是怎么实现的String 的不变性有什么好处”String 类内部用一个char数组或者byte数组存字符它的equals先比较引用、再比较长度、最后逐个字符比较。hashCode则是s[0]*31^(n-1) s[1]*31^(n-2) ...这种散列公式用 31 是因为它是一个奇质数在乘法运算里冲突概率相对低而且 JVM 对31 * i有优化编译器会把它转成(i 5) - i位运算比乘法快。String 的不变性让它可以安全地被字符串常量池复用也保证了作为 HashMap 的 key 时哈希值不会变。这个问题的意义不在背答案而在考察你有没有把“数据结构”和“语言特性”联系起来思考的习惯。1.2 HashMap 的底层结构、扩容机制与并发隐患HashMap 是 Java 面试里当之无愧的“题王”。它的回答版本也在演进JDK 7 及以前是数组加链表JDK 8 开始变成数组加链表加红黑树。默认初始容量是 16负载因子是 0.75。什么意思就是当元素个数超过16 * 0.75 12时会触发扩容容量翻倍到 32。为什么负载因子取 0.75 而不是 1 或者 0.5这是空间和时间的折中。负载因子越高空间利用率越高但哈希冲突概率变大链表长度增加查询效率下降负载因子太低则空间浪费太多。0.75 是官方在大量测试后给的经验值。链表转红黑树的阈值是 8树转回链表的阈值是 6之所以不是同一个数是为了避免元素在边界来回浮动时频繁转换。为什么阈值取 8本质上是一个概率问题在随机哈希函数下链表节点数服从泊松分布负载因子 0.75 时桶中链表长度达到 8 的概率已经低于千万分之一所以 8 是一个“几乎不会出现但出现了就说明哈希分布极差”的信号。HashMap 的线程不安全是最常被追问的。JDK 7 里并发 put 可能导致扩容时链表形成环一旦 get 那个 key 就会死循环CPU 直接飙满。JDK 8 把头插法改成了尾插法在一定程度上避免了环的产生但并发场景下依然有数据覆盖问题两个线程同时判断某个 key 不存在同时插入后写覆盖先写还有一种情况是 put 过程中检查到容量不够多个线程同时扩容导致数据丢失。所以并发场景不要用 HashMap要用ConcurrentHashMap。JDK 8 的 ConcurrentHashMap 放弃了 JDK 7 的分段锁设计改为 CAS 加 synchronized 锁住数组的单个槽位粒度更细并发度更高。我自己面试别人的时候通常会追问一句“为什么 JDK 8 要把扩容后重新散列的算法改成高低位拆分”。JDK 7 扩容是对每个元素重新计算 hash 再确定新位置JDK 8 优化成根据 hash 值新增的那一位是 0 还是 1元素要么留在原索引要么移动到“原索引加旧容量”的位置。这样做既省去了重新计算 hash 的开销也减少了元素移动次数。这个问题能答上来说明你确实读过源码而不是只看面经。2. JVM 与内存模型拉开差距的核心区2.1 运行时数据区域与对象创建机制JVM 这块可以说是大厂面试的分水岭。背完“程序计数器、虚拟机栈、本地方法栈、堆、方法区”这五个名字只是入门关键是能讲清楚每个区域里到底发生了什么。虚拟机栈管的是方法调用的过程。每次调用一个方法JVM 就会创建一个栈帧栈帧里有局部变量表、操作数栈、动态链接、方法出口。方法嵌套调用的深度是有上限的线程栈深度超过限制就会抛StackOverflowError。堆则是一切对象实例和数组的分配场所也是垃圾回收的主战场。方法区在 JDK 8 里被移到了元空间不再使用虚拟机内存而是用本地内存这样设计主要是为了避免永久代的OutOfMemoryError因为元空间默认只受本机可用内存限制并且字符串常量池在 JDK 7 时就已经移到了堆里。对象的创建过程也经常被连环追问类加载检查、分配内存、初始化零值、设置对象头、执行构造方法。其中分配内存那一步指针碰撞和空闲列表分别对应“堆内存规整”和“堆内存不规整”两种情况。如果是并发创建对象JVM 还需要解决线程安全问题主流做法是 CAS 加失败重试或者使用线程本地分配缓冲TLAB。能聊到 TLAB 的候选人一般基础就不会太差。2.2 垃圾回收算法与主流收集器垃圾回收问的是什么我认为核心就两个一是怎么判定对象已死二是怎么把垃圾高效清掉。判定对象已死主流答案是基于可达性分析。从 GC Roots 出发遍历引用链不能被到达的对象就可以被回收。GC Roots 包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、本地方法栈中 JNI 引用的对象等。这里有个经典坑点两个对象互相引用但都没有被外部引用可达性分析依然能判定它们已死而引用计数法做不到所以现代主流的 JVM 都采用可达性分析。顺带一提面试官很可能继续问“强引用、软引用、弱引用、虚引用的区别”软引用在内存不足时会被回收弱引用在下一次 GC 时必被回收虚引用完全不影响生命周期只用于对象回收后的通知。回收算法里标记-清除会产生大量碎片标记-复制要浪费一半空间标记-整理要移动对象。现代的收集器基本都是分代设计新生代对象存活率低适合复制算法老年代对象存活率高适合标记-整理或者标记-清除。CMS 是第一款并发收集器它的初衷是减少停顿但牺牲了 CPU 资源并且会产生浮动垃圾最后还因为无法处理碎片化问题在 JDK 9 里被标记为废弃。G1 把堆划分成一个个 Region可以做到可预测的停顿时间它的核心思路是维护一个优先级列表回收价值最大的 Region 优先。再往后就是 ZGC停顿时间控制在 10 毫秒以内但越是新东西面试里问到深度的概率就越低因为大多数人没有线上压测经验能答出“G1 的 Region 结构、Mixed GC、Remembered Set”已经足够过关。2.3 线上 OOM 排查的实战套路大厂面试基本不满足于让你背概念经常会把问题包装成场景“线上突然 OOM你怎么处理”这类问题没有唯一答案但有一套标准动作。先看报错信息是Java heap space还是Metaspace还是Unable to create new native thread定位是哪块区域出了问题。然后用jps找到进程号jstat -gcutil看各代的使用率和 GC 频率jmap -dump:formatb,fileheap.hprof导出堆快照再用 MAT 或者 JProfiler 分析。分析的时候先看支配树找到占用内存最大的那个对象再看引用链定位到业务代码的哪一行。如果是内存泄漏重点关注是否有全局缓存无限增长、数据库连接关闭失败、IO 流未释放、ThreadLocal 没有 remove。如果是内存溢出而非泄漏一般是大促流量打上来导致对象瞬间暴增这种时候更常用的手段是调整堆参数、增加机器、优化批量查询逻辑。我记得有一次真实排查一个定时任务每天凌晨跑批数据量增长后频繁 Full GC每次 Full GC 后老年代还是很快被打满。用jmap看堆发现一个ArrayList里存了几百万个LogRecord对象。根因是批量处理失败后的重试逻辑里没有分批重试时把失败批次和积压数据全部加载进内存再处理。最后改成游标式分批加载同时限制最大重试批次大小问题就消失了。这类经验在面试里讲出来远比背诵参数列表有说服力。3. 并发编程不只是会用锁而是理解锁3.1 volatile 与 synchronized 的底层运作机制并发这块是三到五年经验的 Java 工程师最常被深挖的地方。先来看最基础也最关键的一道题“volatile 和 synchronized 有什么区别”volatile 有两个语义保证可见性和禁止指令重排序但它不保证原子性。可见性靠的是内存屏障加缓存一致性协议一个线程修改了 volatile 变量后会强制将修改写回主内存并让其他线程中该变量的缓存行失效。禁止指令重排序则是在读写 volatile 变量的地方插入内存屏障防止编译器和 CPU 对指令进行乱序优化。典型的应用场景就是双重检查锁单例模式中的instance字段没有 volatile 的话可能出现一个线程拿到半初始化对象的问题原因在于“分配内存并设置引用”和“调用构造方法初始化对象”这两个步骤可能被重排序。synchronized 则是对一段代码块或者一个方法加锁。它从 JDK 6 开始做了大量优化锁的状态从无锁到偏向锁、轻量级锁、重量级锁是一个逐渐膨胀的过程。偏向锁的意思是“这个锁大概率只被一个线程访问”于是就在对象头里记录持有者的线程 ID省掉 CAS一旦有其他线程竞争就升级成轻量级锁通过 CAS 在栈帧中记录锁记录的地址去竞争竞争激烈时升级成重量级锁未获得锁的线程进入阻塞队列涉及操作系统用户态和内核态切换代价最大。现在的 JDK 版本已经逐步废弃了偏向锁但理解这种演变过程仍然很重要因为它体现了 JVM 的一个核心设计思路针对“锁竞争程度不同”的情况用不同代价的方案去适配。追问的进阶题往往长这样“一个方法里现在既有读操作又有写操作全加上 synchronized 会不会性能很差怎么优化”这就引出了读写锁、乐观锁、CAS、ConcurrentHashMap 分段设计等一堆知识。我建议回答的时候主动往“锁的粒度”和“并发数据结构”这两个方向靠面试官会顺着你的话继续深入答到点上就是加分。3.2 CAS 的原理与 ABA 问题CAS全称 Compare And Swap可以说是并发编程的基石之一。它的操作是比较内存中的值和期望值如果相等就更新成新值整个比较加更新在硬件层面是不可分割的。CAS 避免了锁带来的线程挂起和恢复开销但它不是没有问题。第一CAS 如果一直失败会一直自旋重试在高并发下会白白消耗 CPU。第二CAS 只能保证一个共享变量的原子操作操作多个共享变量时需要加锁或者用 AtomicReference 把它们封装成一个对象。第三就是著名的 ABA 问题线程 1 读到值 A线程 2 把 A 改成 B 又改回 A线程 1 再 CAS 时发现还是 A于是认为没有变化就继续操作了。如果这个过程中其他线程对数据结构产生了影响就可能出问题。ABA 问题怎么解决经典方案是为变量加上版本号每次修改版本号加 1。Java 里的AtomicStampedReference就是干这个的它同时持有引用和版本戳。比如一个账户余额线程 A 看到余额是 100线程 B 转进 50 变成 150又转出 50 变回 100如果没有版本号线程 A 会以为中间没人动过这在某些计费场景下会出大问题。用AtomicStampedReference就能感知到版本的变更。不过实践中大部分 ABA 场景其实影响不大比如店铺库存这种最终还是看当前值的地方它更关心的是即时状态。这点你主动说出来反而会显得有真实项目判断力。3.3 线程池的核心参数与真实调优场景线程池面试题问得极其高频核心就是那七个参数corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler还有一个 BlockingQueue 的实现选择。很多人把参数背得很熟但一追问“你这几个参数在线上怎么定的”就卡住了。先理一遍流程提交任务后当前线程数小于核心线程数就创建新线程执行超过核心线程数任务先进入队列队列也满了继续创建线程直到最大线程数再满执行拒绝策略。这里最容易被误解的是“corePoolSize 满了之后是扩容线程而不是等队列满直接拒”。线程数从 core 扩展到 max 的前提是队列已满。拒绝策略有四种AbortPolicy是直接抛异常CallerRunsPolicy是让提交任务的线程自己跑DiscardPolicy是静默丢弃DiscardOldestPolicy是丢弃队列里最老的任务。实际项目里我一般推荐CallerRunsPolicy它不会丢任务同时也天然提供了一种背压机制当线程池饱和时调用方线程被迫执行任务就自然放慢了提交速度。参数怎么定要看任务是 CPU 密集型还是 IO 密集型。CPU 密集型期望核心线程数接近 CPU 核数通常是N1IO 密集型因为大部分时间在等待网络或磁盘线程数可以多一点常用公式是N * (1 等待时间 / 计算时间)。我曾经调过一个报表导出服务压测时线程数从 20 加到 50吞吐量不升反降因为大量线程同时争抢数据库连接连接池先成了瓶颈。后来把思路反过来先定数据库连接池上限再反推线程池大小同时把导出任务拆成更细的批次放入队列最终吞吐量反而翻倍。面试里能讲清楚这类权衡比单纯背公式有价值得多。4. Spring 与 Spring Boot框架题背后的设计思想4.1 Bean 的生命周期与三级缓存解决循环依赖Spring 题目现在普遍向源码靠拢。第一个高频题“Spring 中 Bean 的生命周期是什么”简单概括就是实例化前阶段BeanPostProcessor 的初始化前置处理、实例化、属性填充、初始化InitializingBean、init-method、使用、销毁。但最好把这个流程挂到具体接口上去理解。比如BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization分别会在初始化前后被调用AOP 的代理对象就是在postProcessAfterInitialization这个阶段生成的。Autowired依赖注入发生在属性填充阶段而PostConstruct注解是在属性填充完成之后才执行。理解了这个顺序你就明白了为什么在构造方法里直接使用Autowired属性会拿到 null因为构造方法执行时机早于属性填充。循环依赖问题也非常经典“Spring 怎么解决循环依赖”注意Spring 解决的是单例 Bean 的 setter 注入循环依赖构造器注入的循环依赖它解决不了会直接抛BeanCurrentlyInCreationException。Spring 的做法是三级缓存第一级是singletonObjects存放完整的单例 Bean第二级是earlySingletonObjects存放提前暴露的、还没完成属性填充的半成品对象第三级是singletonFactories存放 ObjectFactory。为什么需要第三级而不是直接提前创建完整对象因为大部分 Bean 在创建后期会被动态代理如果二级缓存直接放原始对象后续代理对象就没有机会替换了。三级缓存中存的是 ObjectFactory在真正暴露的时候才决定是返回原始对象还是代理对象这样既保证了单例语义又给 AOP 留了口子。答出这一层面试官通常会比较满意。4.2 AOP 的底层代理机制与失效场景AOP 在 Spring 家族里的实践非常广泛从声明式事务到日志切面、权限控制再到Async底层都离不开动态代理。高频问题就是“JDK 动态代理和 CGLIB 有什么区别Spring 里会怎么选”JDK 动态代理基于接口通过Proxy.newProxyInstance生成一个实现同样接口的代理类调用方法时通过InvocationHandler转发CGLIB 则通过继承目标类生成子类重写目标方法。JDK 自带的代理要求目标类必须有接口而 CGLIB 可以代理没有接口的类。Spring Boot 2.x 之后默认使用的是 CGLIB也就是通过proxyTargetClasstrue因为现在很多业务类根本不设计接口直接 CGLIB 更省心。但如果目标方法是 final 的或者类是 final 的CGLIB 也代理不了因为子类无法重写 final 方法。AOP 失效的经典场景是什么最典型的就是同类内部方法调用。Spring 的代理只在从外部调用 Bean 的方法时生效如果 Bean 内部一个方法调用另一个带Transactional的方法走的是this引用没有经过代理对象事务就失效了。解决办法有三个注入自身代理、拆到另一个 Bean 里、使用AopContext.currentProxy()。这些细节在面试里属于“答完原理之后主动补充的提分点”。还有个容易混淆的点Spring 的事务和Async用的是同一个代理机制所以很多Async失效的问题根因和事务失效是一模一样的。4.3 Spring 事务失效的八大典型场景“Spring 事务在什么情况下会失效”这是大厂面试官比较爱出的“场景题”因为业务项目里事务踩坑太常见了。我总结下来有八个典型场景。第一方法不是 public 的Spring 默认用 CGLIB 代理代理只能增强 public 方法非 public 方法的事务注解不生效。第二同类内部调用也就是前面说的不走代理。第三异常被 catch 住吞掉了事务感知不到异常自然无法回滚。第四抛出的异常类型不是 RuntimeException而是 checked Exception比如IOExceptionSpring 默认只对 RuntimeException 回滚checked 异常不会触发回滚除非在Transactional里显式指定rollbackFor。第五数据库引擎不支持事务比如 MySQL 用了 MyISAM 表。第六多线程调用一个方法内开启子线程去执行另一个事务方法因为事务和线程绑定子线程里的事务不会和主线程合并而且子线程抛异常也不会回滚主线程。第七事务方法里使用this调用切面相关的内部方法。第八类没有被 Spring 管理也就是忘了加Service之类的注解。每个场景背后其实都指向同一个核心理解Spring 事务的本质是 AOP本质是通过代理拦截目标方法在方法执行前开启事务、在方法返回或抛出异常时提交或回滚。把这个本质讲清楚八个场景根本不用背自己就能推导出来。5. MySQL 与 Redis数据层的进阶追问5.1 索引为什么选 B 树什么时候索引会失效MySQL 的索引是必考内容而且角度越来越刁钻。最基本的题目是“InnoDB 的索引为什么用 B 树而不用 B 树或者红黑树”B 树相比 B 树的优势是数据只存储在叶子节点内部节点只存键值所以单页可以容纳更多索引项树的层级更矮。三层 B 树大概可以存储千万级别到亿级别的数据量加上叶子节点之间有链表连接做范围查询非常高效。红黑树是二叉树高度远高于 B 树磁盘 IO 次数多完全不适合数据库这种以磁盘块为单位的访问模式。哈希索引虽然能 O(1) 做等值查询但没法做范围查询也无法利用最左前缀。这是从数据结构特性去理解 MySQL 选型的核心逻辑。接着就会被问二级索引和回表。InnoDB 是聚簇索引主键索引的叶子节点直接存整行数据二级索引的叶子节点存的是主键值。用二级索引查询时先找到主键再回聚簇索引查一次这个过程叫回表。如果查询的列正好全部在二级索引里就形成了覆盖索引不需要回表性能会好很多。这就是为什么查询建议只 select 必要列而不是无脑 select *因为一旦带上索引之外的列覆盖索引就用不上了。索引失效的场景也是必背清单对索引列使用函数或者运算发生隐式类型转换比如索引列是字符串查询条件却用了数字使用模糊匹配且通配符在前面LIKE %abc无法走索引LIKE abc%可以使用OR连接非索引列联合索引不满足最左前缀规则。这些规则背下来是基础但更关键的是能从“索引本身就是一种有序结构”的角度去理解为什么失效。比如对索引列做运算本质上是改变了索引列的值原有排序无法再用自然就失效了。5.2 MySQL 的锁机制与事务隔离级别MySQL 并发控制的相关问题核心围绕事务隔离级别和锁。事务的四大特性 ACID 人人都能背关键是隔离级别。MySQL 默认的隔离级别是REPEATABLE READ也就是可重复读。它通过多版本并发控制MVCC实现快照读同时用next-key lock解决幻读问题。这里的 next-key lock 是记录锁和间隙锁的组合它锁定的不只是某一行还包括这行之前的间隙这样别的事务就无法在间隙里插入新数据。理解了这一点就能解释为什么在可重复读级别下某些范围查询会把不相干的记录也锁住从而可能引发死锁。面试时一个更高频的追问是“READ COMMITTED和REPEATABLE READ的区别是什么”简单说前者每次读取都生成新的快照所以同一事务内两次查询结果可能不同后者在事务开始时就生成快照后续读的是同一个快照所以能重复读。但要注意可重复读的快照读并不能完全避免幻影读下的写冲突需要结合 next-key lock 才能做完整防护。这也是为什么很多人说 MySQL 在默认隔离级别下其实已经基本做到了类似串行化隔离级别下才有的防幻读效果。关于死锁面试官很喜欢让候选人现场分析。比如两条 update 语句以相反顺序锁定两行数据就可能形成互相等待。排查死锁的常规动作是执行SHOW ENGINE INNODB STATUS查看 LATEST DETECTED DEADLOCK 部分找到持锁的事务和等待锁的事务。业务上规避死锁的一个重要思路是让所有事务以固定的顺序访问表和行。5.3 Redis 缓存穿透、击穿、雪崩的应对方案Redis 相关的场景题最经典的就是缓存三大问题。缓存穿透是查询一个根本不存在的 key请求打到数据库上如果这种请求量很大数据库压力就会异常。解决思路是缓存空值把不存在的 key 也缓存一下但要设置较短的过期时间更好的是用布隆过滤器在缓存前面加一道判断把明显不存在的 key 直接拦截掉。注意布隆过滤器有误判率它会告诉你“一定不存在”和“可能存在”误判不会漏掉真实存在的 key只会把极少数不存在的 key 放过去这在缓存场景下是可接受的。缓存击穿是某个热点 key 在过期瞬间大量并发请求同时打到数据库。处理办法有三个互斥锁也就是只让一个请求去重建缓存其他请求等待逻辑过期把 value 里塞一个过期时间字段异步线程去更新缓存请求先拿旧值返回永不过期加后台定时刷新。互斥锁实现简单但存在串行化问题逻辑过期能做到最终一致且不阻塞请求但对业务代码有侵入。缓存雪崩是大量 key 同时过期或者 Redis 实例宕机导致请求全部落到数据库。应对策略包括过期时间加一个随机值避免同一时刻集体过期用多级缓存本地缓存加分布式缓存Redis 高可用部署加上熔断降级策略。面试官通常会追问“如果 Redis 挂了你怎么办”这时候不要只答“等它恢复”而要把兜底方案讲出来本地缓存临时顶上、对非核心接口做降级、数据库提前做读写分离扩容。6. 分布式与场景题没有标准答案的加分项6.1 分布式锁Redis 锁、ZooKeeper 锁与 Redlock 之争到了这个层面面试题逐渐变成“开放题”。第一个典型的开放问题是“你在项目里怎么设计一个分布式锁”如果用的是 Redis最简单的方案是SET key value EX seconds NX只有键不存在时才能设置成功过期时间保证锁不会永久持有。但这里面有几个坑。第一个坑是锁的 value 必须带上一个唯一标识比如 UUID释放锁时要先比较 value 再删除否则可能出现线程 A 的锁到期自动释放线程 B 拿到了新锁然后 A 又去把 B 的锁删掉的情况。第二个坑是主从切换时的丢锁问题Redis 主节点宕机后从节点顶上如果锁还没来得及同步到从节点锁就丢了。Redlock 算法试图通过向多个独立 Redis 节点加锁来解决这个问题但它在极端情况下仍有争议比如发生时钟跳跃时。面试时主动讲出 Redlock 的适用场景和争议点会让面试官觉得你真的在生产环境里思考过。如果用 ZooKeeper 做分布式锁常见做法是创建临时顺序节点每个客户端注册后得到一个序号只有序号最小的持有锁其他客户端监听前一个节点。临时节点的好处是客户端宕机后节点自动消失不会出现 Redis 锁那种“死锁后只能等过期”的问题。但 ZooKeeper 锁的性能不如 Redis而且每次加锁都要多次网络交互。实际选型逻辑是要求高可靠性和自动释放就选 ZooKeeper追求性能和简单性就选 Redis。这个问题没有唯一答案你把自己项目的访问量、可用性要求、已有基础设施说清楚就是完整的回答。6.2 接口幂等性与重复提交的处理“怎么保证接口的幂等性”是分布式场景的常客。面试题包装通常是“用户快速点了两次提交后端重复扣款了怎么避免”。幂等性设计的核心思想是让一次和多次请求的效果相同。最简单也最可靠的方法是在数据库层面加唯一约束比如订单号唯一、支付流水号唯一。第一次插入成功第二次插入会因为唯一键冲突而失败业务代码里捕获这个冲突返回“处理中”或者“已成功”即可。对于更新场景可以用乐观锁表里加一个版本号update 时带上version ?条件更新成功后版本号加一更新行数为 0 就说明版本不匹配直接丢弃该次请求。还有一种思路是使用状态机。比如支付单的状态从“待支付”到“支付中”再到“已支付”是一个不可逆的过程业务代码里通过update ... where status 待支付这样的条件更新来保证同一状态只能被成功推进一次。这种方案的优点是语义清晰缺点是业务代码里到处都要写状态判断侵入性强。面试里你只要讲清楚场景匹配哪种方案比如“下单用唯一键扣库存用乐观锁长流程用状态机”就比只背一个方案要出彩。6.3 消息队列的可靠性不丢、不重、有序消息队列一定是分布式简历里的高频组件。高频题是“怎么保证消息不丢失”回答这个问题要分三段来拆。生产端使用同步发送并等待 Broker 确认或者开启事务消息、确认回调保证消息真正到达 BrokerBroker 端开启持久化把消息刷到磁盘并且对应的队列设置为持久化队列消费端关闭自动 ack改成手动 ack处理成功后再确认。只要三段每一段都有确认机制消息就能基本做到不丢。注意消息队列的不丢是有条件的不丢不同中间件默认配置不一样比如 Kafka 的配置要关注acks、min.insync.replicas和enable.auto.commit三个参数。“怎么保证不重复消费”这才是更难的问题。因为网络重试、消费者宕机再恢复都会导致同一消息被投递多次。消息中间件只能保证至少一次语义无法保证刚好一次所以业务侧必须做幂等这又绕回了上一个话题数据库唯一键、Redis setnx、状态机都天然可以用来做消费幂等。“消息怎么保证顺序性”Kafka 的答案是分区有序同一业务 key 的消息发到同一个分区消费者在分区内顺序处理。但一旦有多个消费者并发消费同一个分区顺序就会乱。所以实践中通常还要配合“单分区单消费者”或者把并发度收敛再在业务侧用状态机做兜底。这三类问题串起来答能明显增加面试的完整度。7. 面试现场加减分项聊几个实操心得7.1 答题节奏与“主动引导”最后再分享几个我既作为面试官、也作为被面试者总结出来的实操体会。第一答题要学会分层。面试官问一个概念先给一句话结论再展开细节不要上来就倒一大堆。比如问“HashMap 和 ConcurrentHashMap 有什么区别”先说“线程安全的差异和底层实现不同”再分别展开。提纲挈领的答法让面试官可以随时打断你或者沿着你的话问下去节奏更可控。第二主动暴露“边界”。说完了常规实现补一句“但是这个方案在某某场景下有局限”面试官通常会被带着往你准备好的方向问。比如聊 Redis 分布式锁你主动提主从切换丢锁问题话题就很自然滑向 Redlock 和 CAP这些都是你能提前准备的。千万不要只等面试官出招那样很容易被问到知识盲区。第三不会的题要表现推导过程。大厂面试官其实不是非要你每道题都答对他们更关注你面对不确定问题的反应。遇到不会的题先把已知的部分拆解出来再尝试类比到熟悉的东西上。比如让你说一个没接触过的中间件的实现原理你可以从“它解决什么问题、有什么类似物、数据怎么流转”三个角度推。哪怕推得不够准确也比沉默或瞎编强。7.2 简历和项目经验的技术锚点还有一个容易被低估的环节项目描述里提到的每个技术名词都要准备好被追问。写“用了 Redis 缓存”就要准备缓存穿透、击穿、雪崩、缓存一致性这几个问题写“用消息队列做了异步解耦”就要准备可靠性、顺序性、积压处理这些问题。项目里用到了什么技术面试官默认你会往深了聊所以不要在简历上堆不认识的名词。我建议把项目经验按“背景、方案、难点、结果”四段式准备其中“难点”是重中之重。难点最好是那种能在技术层面体现出取舍的问题比如“订单量上涨后数据库查询变慢你是怎么优化的”。这种问题既考察技术宽度也考察你是不是真的参与了这个项目。把优化前后的数据对比也准备好有数据支撑的回答说服力完全不一样。准备面试本质上就是在整理自己的知识体系。刷题只是引子真正重要的还是把每个问题的“为什么”想清楚。我自己的体会是把上面这几条线吃透不管面试题拿到的是哪套组合都能找到对应的思考路径。把心态放稳把基础夯实金三银四的机会自然会落到有准备的人手里。