轨道 · 写下来的字
从练气到元婴:Java 后端面试修仙指南
口诀记框架,原理见真章。
一、速览口诀
先用口诀认清各阶段的主题,再到第二部分看原理、场景和常见误区。口诀用于回忆关键词,不能代替具体版本下的实现细节。
练气期(八股基础)
Java 基础
基础先查栈与堆,
包装别忘拆装箱。
重写重载要分清,
值传引用莫发昏。
JVM
内存分区要记清,堆栈方法区分明。
可达分析找垃圾,分代回收G1灵。
类加载有双亲委派,父先加载子再来。
并发
线程锁得理分明,
可见有序原子行。
Volatile知其本性,
不保原子需记清。
池化技术省资源,
死锁四个条件全。
集合
集合家族理周全,
HashMap是必问点。
红黑树转记边界:八转树来六退链,数组六十四为限。
扩容阈值在心间:负载七五,翻倍添。
Linked增删快,随机访问慢;
ArrayList查询快,中间增删要移位。
Spring
Spring来把注解寻,
IOC反转控依存。
AOP切面搞增强,
事务传播别迷茫。
失效场景要记详:非public、自调用,吞异常、回滚类别错。
不然面试徒悲伤。
MySQL
MySQL里探本源,
索引B+树为先。
最左匹配不能偏,
回表查询耗时间。
事务隔离分四级,
锁的粒度要知悉。
避免函数致失效,
不要乱建一堆索引。
Redis
Redis中解迷局,
五大类型记有余。
过期淘汰策不移,
击穿穿透和雪崩。
持久两种做区分,
集群模式要留神。
消息队列
MQ重点解三高,
削峰解耦和异步。
消息丢失三段管,
重复消费靠幂等。
分布式
分布式,锁争忙,
缓存一致性断肠。
限流熔断加降级,
高可用里藏玄机。
筑基期(AI 与架构进阶)
AI 应用
基础知识点要稳,
莫把AI当门神。
先谈Prompt怎么引,
再控幻觉免失准。
Agent
Agent链路理分明,
工具调用要小心。
规划检索做记忆,
上下文里藏危机。
向量库
向量库,选引擎,
分片距离要厘清。
embedding做比对,余弦相似最常用。
维数太高成本累,HNSW与IVF各有用。
RAG
RAG不单是入库,
重排检索调粒度。
切片策略多权衡,
漏查冗余要平衡。
混合检索加改写,评测指标不能少。
高并发
高并发,先锁根,
取舍CAP莫较真。
最终一致何时稳,
强一致下代价深。
两阶段提交等协调,多数派上 Raft。
流量治理
流量突增看预案,
限流熔断和降权。
热点击穿防震荡,
缓存策略要周全。
分布式事务
分布式,寻痛点,
事务一致性难管。
TCC消息Saga选,
业务场景做判断。
可观测
可观测,三件套,
日志链路指标照。
故障溯源别瞎找,
埋点采样要精妙。
压测
压测当中寻瓶颈,
资源成本要细盯。
不要盲目堆机器,
优化算法减压力。
选型与项目
技术选型莫逞强,
投入产出要掂量。
知其优点知其伤,
适配业务方为上。
谈项目,忌空谈,
风险取舍是重点。
遇到坑怎么排难,
沉淀方案可复盘。
套用STAR讲清楚,量化指标是关键。
AI 赋能工程
讲AI赋能工程,
不是复制就完工。
脚本生成省工时,
评审把关不能松。
安全
安全防线不能放,
注入越权要严防。
密钥脱敏做屏障,
泄露一出祸难当。
金丹期(系统设计)
开局:问与算
系统设计莫慌张,
先问需求再估量。
QPS存储与带宽,
粗算一遍心有章。
架构:拆与分
分层拆分要清晰,
接入服务存储立。
无状态服务可横扩,
有状态处要谨慎。
数据:库与表
库表拆分看瓶颈,
垂直水平各有宜。
分片键选要均匀,
跨片事务尽量避。
全局ID用雪花,时钟回拨要防它。
读写:缓与异
缓存多级层层挡,
本地加上Redis。
读多写少最相宜,
一致性要先定。
异步削峰靠队列,
写入落库慢慢消。
接口幂等不能少,
唯一键与状态机。
秒杀五字诀
拦在前,削在中,缓来挡,扣要准,异步下单落库稳。
上线:灰与守
灰度发布小步走,
回滚预案提前备。
监控告警全覆盖,
容灾多活保平安。
元婴期(技术视野与团队)
规划
技术规划看三年,
业务目标是基线。
技术债务要偿还,
不可只管眼前田。
成本
成本收益常盘点,
降本增效有数据。
选型不迷信新技术,
只选对的不选潮的。
团队
团队梯队要搭建,
代码评审传经验。
事故复盘不追责,
沉淀机制比人强。
表达
对上讲目标,对下讲方法,
对外讲价值,对己讲复盘。
二、教学篇:把口诀讲成答案
口诀只给关键词,面试要的是能展开的答案。本篇把每一句口诀拆成四步:是什么、为什么、怎么用、有什么坑。
答题万能框架
- 一句话定义:先给结论,例如“HashMap 是基于哈希表的键值容器,JDK 8 为数组加链表加红黑树”。
- 讲原理:说清核心机制,例如哈希计算、冲突处理、扩容。
- 落场景:说明何时用、何时不用,最好带上自己项目中的例子。
- 谈取舍与坑:线程安全、性能边界、常见误用,这一步最能拉开差距。
怎么读本篇
- 练气期每个主题都有“核心考点、原理讲透、面试答法、易错点”。第一遍只读加粗的结论,第二遍再读原理。
- 每个主题末尾的“追问”是面试官最常继续问的问题,请自己先答一遍,再对照文中内容。
- 文中的实现细节以常见版本为例(如 JDK 8、Spring 6、MySQL InnoDB、Redis 常见版本);JVM、框架和数据库的行为会随版本及配置变化,回答时应说明自己讨论的环境。
练气一:Java 基础与 JVM
核心考点:栈与堆、拆装箱、重写重载、值传递、垃圾回收、类加载。
1. 栈与堆
- Java 虚拟机栈是线程私有的,每次方法调用对应一个栈帧,保存局部变量表、操作数栈和方法返回信息。栈溢出(StackOverflowError)常见于递归过深等情况。
- 堆是线程共享的,所有对象实例都在这里分配。堆分为新生代(Eden 加两个 Survivor)和老年代。堆不足时抛 OutOfMemoryError。
- 方法区是 JVM 规范中的运行时区域;HotSpot 在 JDK 8 起用元空间实现类元信息的存储。不要把规范概念与某个 JVM 实现的内存布局完全等同。
2. 拆装箱
- 装箱是基本类型转包装类,调用
Integer.valueOf;拆箱是反向,调用intValue。 Integer默认缓存 -128 到 127 的值,因此常见 JDK 配置下,Integer a = 127, b = 127; a == b为 true,而 128 通常为 false;缓存上界可受 JVM 配置影响。比较包装类的数值应使用equals,并先处理 null。- 坑:包装类为 null 时拆箱会抛 NullPointerException,三目运算、自动求和等场景要留意。
3. 重写与重载
| 对比 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类内 |
| 方法签名 | 完全相同 | 参数列表不同 |
| 绑定时机 | 运行时(多态) | 编译期 |
| 返回值与异常 | 返回值可协变,异常不能更宽 | 不作要求 |
4. 值传递
Java 只有值传递。基本类型传值的副本;对象传的是引用的副本。因此方法里可以修改对象的字段,但让形参指向新对象不会影响外部的引用。
5. 垃圾回收
- 找垃圾:可达性分析,从 GC Roots(栈中引用、静态变量、常量等)出发,不可达的对象即可回收。引用计数法无法解决循环引用,所以主流虚拟机不用。
- 分代回收:新生代对象大多朝生夕灭,用复制算法;老年代存活率高,用标记清除或标记整理。
- 常见收集器:G1 兼顾吞吐和停顿,是 JDK 9 起的默认收集器;ZGC 追求极低停顿,适合大堆。
- 调优思路:先看 GC 日志,确认是 Young GC 频繁还是 Full GC,再找内存泄漏或不合理的对象分配。
6. 类加载与双亲委派
- 流程:加载、验证、准备、解析、初始化。
- 双亲委派:类加载请求先交给父加载器,父加载器无法加载时子加载器才会尝试。好处是避免核心类被篡改,并保证类的唯一性。
面试追问
- 为什么 String 要设计成不可变?(安全、可缓存哈希、线程安全、常量池复用)
- Full GC 频繁怎么排查?(看 GC 日志,用 jmap、MAT 分析堆,找大对象和泄漏点)
练气二:并发
核心考点:并发三特性、volatile、synchronized 与 ReentrantLock、线程池、死锁。
1. 三大特性
- 原子性:一个操作不可被打断。
i++包含读、加、写三步,不是原子操作。 - 可见性:一个线程修改共享变量后,其他线程能立即看到。
- 有序性:编译器和 CPU 可能重排指令,多线程下会产生意料之外的结果。
Java 内存模型(JMM)用 happens-before 规则保证这三点。
2. volatile
- 保证可见性:一个线程对
volatile变量的写入,对之后读取该变量的线程可见;具体语义由 Java 内存模型定义,不宜简化成每次都直接读写物理主存。 - 保证一定的有序性:对
volatile变量的读写建立相应的 happens-before 关系,限制相关重排序。典型场景是双重检查锁单例中的instance变量。 - 不保证原子性:两个线程同时执行
count++,仍会丢失更新。需要原子性时用AtomicInteger(CAS)或加锁。
3. synchronized 与 ReentrantLock
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 层面 | JVM 关键字 | JDK 类库 |
| 释放 | 自动释放 | 必须在 finally 中手动释放 |
| 功能 | 基础互斥 | 可中断、可超时、可公平、可绑定多个条件 |
| 使用建议 | 优先使用,简单可靠 | 需要高级功能时使用 |
4. 线程池
七个核心参数:核心线程数、最大线程数、存活时间、时间单位、任务队列、线程工厂、拒绝策略。
执行流程:
- 任务到来,核心线程未满,则创建核心线程执行。
- 核心线程已满,任务进入队列。
- 队列已满,且未到最大线程数,则创建非核心线程。
- 都已满,触发拒绝策略(抛异常、调用者执行、丢弃、丢弃最老任务)。
坑:不要用 Executors 直接创建线程池,newFixedThreadPool 的队列无界,可能堆积任务导致内存溢出。应使用 ThreadPoolExecutor 显式指定参数。
5. 死锁
四个必要条件:互斥、持有并等待、不可剥夺、循环等待。
- 预防:所有线程按相同顺序获取锁(破坏循环等待);使用带超时的
tryLock(破坏持有并等待)。 - 排查:用
jstack查看线程栈,会直接提示 Found one Java-level deadlock。
面试追问
- synchronized 的锁实现经历过哪些优化?回答时结合目标 JDK 版本说明;偏向锁已在较新 JDK 中弃用,不能把旧版的“无锁、偏向、轻量级、重量级”当作所有版本固定不变的升级流程。
- CAS 有什么问题?(ABA 问题、自旋开销、只能保证单个变量的原子性。)
- 线程池核心线程数怎么设?(CPU 密集型约为 CPU 核数加 1;IO 密集型可设为核数的数倍,最终要压测验证。)
练气三:集合
核心考点:HashMap 结构与扩容、ArrayList 与 LinkedList、线程安全集合。
1. HashMap(JDK 8)
- 结构:数组加链表加红黑树。数组的每个格子叫桶,键经过哈希定位到桶。
- 哈希计算:取
key.hashCode()并把高 16 位异或到低 16 位(扰动函数),再与“数组长度减 1”做与运算得到下标。数组长度必须是 2 的幂,这样与运算等价于取模且更快。 - 冲突处理:同一个桶内用链表串起来,JDK 8 采用尾插法。
- 树化与退化:链表达到树化阈值(默认 8)且数组长度至少 64 时才会转红黑树;数组较小时优先扩容。退化阈值(默认 6)用于避免链表与红黑树在边界附近频繁转换,具体触发时机与节点移除或扩容过程有关。
- 扩容:元素数超过“容量乘负载因子”(默认 16 乘 0.75,即 12)时,容量翻倍并重新分布节点。扩容有成本,已知数据量时应预设初始容量。
为什么树化阈值是 8、退化阈值是 6? 这是 JDK 8 HashMap 的实现选择:树化用于应对较长冲突链,退化阈值与树化阈值留出间隔,减少边界附近反复转换。面试时说明这是实现参数,不必把它当成所有 Map 实现通用的规律。
2. ArrayList 与 LinkedList
| 对比 | ArrayList | LinkedList |
|---|---|---|
| 底层 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 尾部追加 | 均摊 O(1) | O(1) |
| 中间增删 | O(n),要移动元素 | 找到位置后 O(1),但定位本身是 O(n) |
| 内存 | 连续,缓存友好 | 每个节点额外存两个指针 |
实践中 ArrayList 在绝大多数场景更快,LinkedList 很少是最优选择。遍历 LinkedList 要用迭代器或增强 for,不要循环 get(i),否则整体变成 O(n²)。
ArrayList 扩容:默认扩容为原来的 1.5 倍,需要拷贝数组。
3. 线程安全集合
- HashMap 在多线程下可能丢数据,JDK 7 还会因头插法在扩容时形成环形链表导致死循环。
ConcurrentHashMap:JDK 8 采用数组加链表加红黑树,用 CAS 加 synchronized 锁住桶的头节点,粒度比分段锁更细。CopyOnWriteArrayList:写时复制,适合读多写极少的场景。Collections.synchronizedMap:整体加锁,并发度低。
面试追问
- HashMap 的 key 用自定义对象要注意什么?(同时重写
equals和hashCode,且参与计算的字段不应可变。) - 为什么重写
equals必须重写hashCode?(相等的对象必须有相同哈希,否则会落到不同的桶,导致查找失败。) - ConcurrentHashMap 的 size 为什么只是近似值?(并发下计数使用分散的计数单元,汇总时可能与瞬时状态不一致。)
练气四:Spring
核心考点:IOC、AOP、事务传播、事务失效。
1. IOC 与依赖注入
- IOC(控制反转):对象的创建和依赖关系交给容器管理,业务代码不再自己
new。 - 依赖注入的三种方式:构造器注入(官方推荐,依赖不可变且便于测试)、Setter 注入、字段注入。
- Bean 的生命周期:实例化、属性填充、初始化(
@PostConstruct、InitializingBean)、使用、销毁。 - 循环依赖:Spring 用三级缓存解决单例 Bean 的 Setter 或字段注入循环依赖;构造器注入的循环依赖无法解决。
2. AOP
- 原理:运行时通过动态代理给目标对象织入增强逻辑。目标实现了接口时默认可用 JDK 动态代理,否则用 CGLIB(Spring Boot 默认优先 CGLIB)。
- 常见用途:日志、权限校验、性能统计、事务。
- 概念:切面(Aspect)、切点(Pointcut)、通知(Advice:前置、后置、环绕、异常、返回)。
3. 事务传播行为
共 7 种,最常考前三种:
| 传播行为 | 含义 | 典型场景 |
|---|---|---|
| REQUIRED(默认) | 有事务则加入,没有则新建 | 大多数业务方法 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 | 日志记录,不受主流程回滚影响 |
| NESTED | 在当前事务内嵌套子事务(保存点) | 子操作失败仅回滚自己 |
其余包括 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER。
4. 事务失效场景(必背)
- 代理类型和方法可见性不匹配:JDK 接口代理只拦截接口中定义的 public 方法;Spring 6 的类代理默认也可处理 protected 和包可见方法。
final方法无法被子类代理覆盖,static方法本身不参与实例代理。 - 同类自调用:
this.methodB()不经过代理,事务不生效。解决:拆到另一个 Bean,或注入自身代理。 - 异常被
catch吞掉,没有再抛出。 - 默认只回滚 RuntimeException 和 Error,抛检查型异常需配置
rollbackFor。 - 对象没有交给 Spring 管理(忘了加
@Service等注解)。 - 数据库引擎不支持事务(例如 MyISAM)。
- 传播行为设置不当,例如使用了 NOT_SUPPORTED。
- 多线程中新开线程执行的数据库操作,不在原事务内。
面试追问
- @Transactional 的原理?(基于 AOP,代理对象在方法前开启事务,方法正常结束提交,抛异常回滚。)
- Spring 的 Bean 默认是线程安全的吗?(默认单例,不是线程安全的,Bean 中不要保存可变的共享状态。)
- BeanFactory 与 ApplicationContext 的区别?(后者是前者的扩展,提供国际化、事件、资源加载等能力。)
练气五:MySQL
核心考点:B+ 树索引、最左匹配、回表与覆盖索引、事务隔离、锁、索引失效。
1. 为什么用 B+ 树
- 非叶子节点只存键,一个节点能放更多键,树更矮,磁盘 IO 次数少(三层通常可存数千万行)。
- 叶子节点存放数据,并用链表相连,范围查询和排序很高效。
- 对比 B 树:B 树的非叶子节点也存数据,扇出更小;对比哈希:哈希不支持范围查询。
2. 聚簇索引、回表与覆盖索引
- InnoDB 的主键索引是聚簇索引,叶子节点存整行数据。
- 二级索引的叶子节点存主键值。通过二级索引查到主键后,再到主键索引取整行,这一步叫回表。
- 查询所需的列都在二级索引里,就无须回表,称为覆盖索引,可以显著提速。
3. 最左匹配
联合索引 (a, b, c) 按 a、b、c 的顺序排序。
- 能用到索引:
a、a,b、a,b,c、a加c(只用到 a 的部分)。 - 不能用到:只查
b或c。 - 范围查询列之后的列无法继续使用索引,例如
a=1 and b>2 and c=3,c 用不上。 - 建联合索引时,把区分度高、等值查询的列放前面。
4. 事务隔离级别
共四级,InnoDB 默认是可重复读:
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不会 | 可能 | 可能 |
| 可重复读 | 不会 | 不会 | 普通一致性读使用同一快照;锁定读是否阻止幻读取决于范围锁和索引 |
| 串行化 | 不会 | 不会 | 不会 |
MVCC 通过 undo log 的版本链和 ReadView 实现一致性读。在 InnoDB 默认的可重复读级别下,同一事务的普通一致性读复用第一次一致性读建立的快照;读已提交则每次一致性读建立新快照。锁定读(如 SELECT ... FOR UPDATE)与普通一致性读的行为不同,间隙锁和临键锁主要用于锁定读、更新和删除等操作;是否阻止插入取决于查询条件和索引。
5. 锁的粒度
- 表锁:开销小,并发低。
- 行锁:InnoDB 通常锁定索引记录;范围条件、索引选择和隔离级别会影响锁住的记录与间隙。没有合适索引时,扫描和加锁范围可能扩大,不能简单概括为“退化成整表锁”。
- 间隙锁与临键锁:用于防止幻读,范围查询时会锁住区间。
- 死锁:多个事务相互等待对方的锁。避免方法是统一加锁顺序、缩短事务。
6. 索引失效的常见场景
- 对索引列使用函数或运算,如
where date(create_time) = ...、where id + 1 = 5。 - 隐式类型转换,如字符串列用数字去比较。
like以通配符开头,如like '%abc'。or连接的条件中有一个没有索引。- 违反最左匹配。
- 优化器判断全表扫描更快(例如数据量小或区分度很低)。
7. 慢 SQL 排查
用 explain 看 type(避免 ALL)、key(实际使用的索引)、rows(预估扫描行数)和 Extra(出现 Using filesort、Using temporary 要警惕)。
面试追问
- 为什么不建议用过长的字符串或 UUID 做主键?(二级索引都会存主键值,占空间;非自增主键会导致页分裂和随机写入。)
- 索引是不是越多越好?(不是。索引占空间,且每次写入都要维护,应按查询场景建少而精的索引。)
- 深分页怎么优化?(用上一页最后的主键做条件,或先通过覆盖索引查出主键再回表。)
练气六:Redis
核心考点:数据类型、过期与淘汰、缓存三大问题、持久化、集群。
1. 五大基本类型与场景
| 类型 | 典型场景 |
|---|---|
| String | 缓存对象、计数器、分布式锁 |
| Hash | 存储对象的各个字段,如用户信息 |
| List | 消息队列、最新列表 |
| Set | 去重、共同好友、抽奖 |
| ZSet | 排行榜、延迟队列 |
2. 为什么 Redis 快
基于内存;单线程执行命令,避免线程切换和锁竞争;使用 IO 多路复用;数据结构经过专门优化。注意:Redis 6.0 起网络 IO 可多线程,但命令执行仍是单线程。
3. 过期删除与内存淘汰
- 过期删除:惰性删除(访问时检查)加定期删除(周期性抽样)。
- 内存淘汰:内存达到上限后按策略淘汰。常见策略有
noeviction、allkeys-lru、volatile-lru、allkeys-lfu、allkeys-random等。缓存场景常用allkeys-lru或allkeys-lfu。
4. 缓存三大问题
| 问题 | 含义 | 解决办法 |
|---|---|---|
| 穿透 | 查询不存在的数据,请求每次都打到数据库 | 布隆过滤器;缓存空值(设置较短过期时间);参数校验 |
| 击穿 | 某个热点 key 过期的瞬间,大量请求涌向数据库 | 互斥锁重建缓存;热点 key 逻辑过期或永不过期 |
| 雪崩 | 大量 key 同时过期,或 Redis 整体不可用 | 过期时间加随机值;多级缓存;Redis 高可用;限流降级 |
5. 持久化
- RDB:定时生成快照。文件小,恢复快,但两次快照之间的数据可能丢失。
- AOF:记录每条写命令,可配置每秒刷盘。数据更安全,文件更大,会通过重写压缩。
- 生产环境常同时开启;Redis 4.0 起支持混合持久化,兼顾恢复速度和安全性。
6. 集群模式
- 主从复制:读写分离,数据备份,但主节点故障需手动切换。
- 哨兵(Sentinel):监控主从,主节点故障时自动选举并切换。
- Cluster:把数据分到 16384 个哈希槽,多个主节点分担读写,支持横向扩展。
7. 缓存与数据库一致性
常用旁路缓存模式:读时先读缓存,未命中再读库并回填;写时先更新数据库,再删除缓存。极端并发下仍可能短暂不一致,可用延迟双删,或订阅 binlog 异步删除缓存,并给缓存设置过期时间兜底。不要“先删缓存再更新数据库”,并发读更容易把旧值写回缓存。
面试追问
- 大 key 的危害?(阻塞、网络拥塞、内存不均,应拆分并避免一次读取过多元素。)
- Redis 分布式锁有什么坑?(要设置过期时间、用唯一值校验释放、考虑锁过期但任务未完成,可用 Redisson 的看门狗续期。)
- 为什么不用
KEYS *?(会遍历全部 key 阻塞服务,应使用SCAN。)
练气七:消息队列与分布式基础
核心考点:MQ 的作用、消息不丢失、重复消费、分布式锁、限流熔断。
1. MQ 解决什么问题
- 削峰:流量高峰时请求先进队列,下游按能力消费。
- 解耦:上游只管发消息,不关心下游有几个消费者。
- 异步:非核心流程(发短信、记日志)异步处理,缩短主链路耗时。
- 代价:系统复杂度上升,需要处理消息丢失、重复、顺序和积压。
2. 消息不丢失:三个环节都要管
- 生产者:开启发送确认(如 RabbitMQ 的 confirm,Kafka 的
acks=all),失败要重试。 - Broker:消息持久化,多副本同步,避免单点。
- 消费者:处理成功后再手动 ack,不要自动 ack。
3. 重复消费与幂等
网络重试、Rebalance 等都会造成重复投递,只能保证“至少一次”,所以消费端必须幂等:
- 用唯一业务号(订单号、消息 ID)加去重表,插入冲突则跳过;
- 利用状态机,只允许合法的状态流转;
- 数据库层面用唯一索引或乐观锁(版本号)。
4. 顺序消息与积压
- 顺序:把需要保序的消息发到同一个分区或队列(如按订单号取模),并且同一队列只由一个消费者线程处理。
- 积压:先定位原因(消费者故障还是处理变慢),再扩容消费者、临时增加分区或降级非核心逻辑。
5. 分布式锁
| 方案 | 做法 | 注意点 |
|---|---|---|
| Redis | SET key 唯一值 NX PX 过期时间,释放时校验唯一值(用 Lua 保证原子) | 锁过期但任务未完成,可用 Redisson 看门狗续期;主从切换可能丢锁 |
| ZooKeeper | 创建临时顺序节点,序号最小者获得锁 | 可靠性高,性能不如 Redis |
| 数据库 | 唯一索引或悲观锁 | 简单,但性能差,只适合低并发 |
6. 限流、熔断与降级
- 限流算法:固定窗口(有临界突刺)、滑动窗口、漏桶(匀速流出)、令牌桶(允许一定突发)。
- 熔断:下游错误率或超时超过阈值时,一段时间内直接失败,保护调用方,之后半开探测恢复。
- 降级:资源紧张时关闭非核心功能或返回兜底结果,例如推荐位显示默认内容。
三者关系:限流防止被打垮,熔断防止被拖垮,降级保住核心。
面试追问
- Kafka 为什么快?(顺序写磁盘、零拷贝、批量发送与压缩、分区并行。)
- 如何保证消息“恰好一次”?(通常是“至少一次”加幂等消费,实现语义上的恰好一次。)
筑基一:AI 应用、Agent 与 RAG
核心考点:Prompt 设计、幻觉控制、Agent 链路、向量检索、RAG 全流程。
1. Prompt 怎么写
Prompt 可以写清任务、必要背景、约束和输出格式;有代表性示例时再加入示例。复杂任务可要求模型给出简要依据或可核对的处理步骤,并在程序侧校验输出格式(如 JSON),不要把模型自述的推理过程当作正确性的证明。
2. 如何控制幻觉
- 给依据:用检索或工具提供可靠上下文,并要求“只依据资料回答,资料中没有就说不知道”。
- 要引用:让答案附上来源片段,便于核对。
- 做校验:关键结论用规则、二次调用或人工审核验证;结构化输出用 Schema 校验。
- 降随机性:事实类任务降低采样温度。
3. Agent 的链路
Agent 是让模型自主决定下一步动作的循环:感知输入,规划任务,调用工具,观察结果,再继续或结束。
- 规划:把目标拆成子任务。
- 工具调用:模型输出结构化的调用请求,由程序执行并回传结果。
- 记忆:短期记忆是上下文,长期记忆常用向量库保存。
- 风险与对策:工具调用失败要有重试和超时;设置最大步数避免死循环;上下文过长要做摘要或裁剪;高风险动作(删除、转账)要人工确认。
4. 向量库与检索
- Embedding:把文本映射为定长向量,语义相近的文本向量距离更近。
- 相似度:常用余弦相似度或内积。
- 索引:HNSW(图索引,召回高、查询快,占内存多);IVF(先聚类再搜索,省内存,召回略低);数据量不大时可直接暴力检索。
- 成本:维度越高,存储和计算越贵,需要在效果和成本之间权衡。
5. RAG 全流程
- 文档处理:解析、清洗、保留标题和表格结构。
- 切片:切太大,噪声多且占上下文;切太小,丢失语义。常见做法是按语义或标题切分,并保留少量重叠。
- 向量化与入库:同时保存元数据(来源、章节、时间),便于过滤。
- 检索:向量检索加关键词检索的混合检索,通常比单一方式召回更全。
- 重排:用重排模型对候选结果精排,选出最相关的几条。
- 生成:把片段和问题一起交给模型,并要求引用来源。
常见问题与优化
| 现象 | 可能原因 | 做法 |
|---|---|---|
| 答非所问 | 查询表述模糊 | 查询改写、拆分子问题 |
| 漏检 | 切片不当,只用向量检索 | 调整切片,混合检索 |
| 冗余、噪声多 | 召回过多,没有重排 | 加重排,控制进入上下文的条数 |
| 知识过期 | 索引未更新 | 增量更新,按时间过滤 |
评测:分别评估检索(命中率、召回率)和生成(答案准确性、是否忠于资料),建立固定的测试集,每次改动都回归比较。
面试追问
- RAG 与微调怎么选?(知识频繁更新、需要可溯源用 RAG;要改变风格或固化技能可考虑微调,两者可以结合。)
- 如何防御提示词注入?(隔离不可信内容、最小权限、输出校验、敏感操作二次确认。)
筑基二:高并发、分布式事务、可观测与安全
1. 高并发与 CAP
- CAP:一致性(C)、可用性(A)、分区容错性(P)三者在网络分区时不可兼得。分布式系统必须容忍分区,所以实际是在 C 与 A 之间取舍。
- BASE:基本可用、软状态、最终一致,是互联网业务常见的折中。
- 一致性协议的代价:两阶段提交需要协调者和参与者完成准备、提交两个阶段;协调者故障时可能阻塞未完成的事务。Raft、Paxos 属于共识协议,通过多数派确认来提交日志;多数派可用时能容忍少数节点故障,但写入需要多数派响应,网络分区时少数派无法继续提交。
- 取舍方法:资金类业务优先一致性,浏览、推荐类业务优先可用性。
2. 流量突增预案
- 事前:容量评估、压测、扩容预案、热点数据预热。
- 事中:限流(入口与关键接口)、熔断、降级非核心功能、队列削峰。
- 事后:复盘,补监控和预案。
热点 key 的处理:本地缓存加 Redis 多级缓存、热点 key 拆分多副本、互斥锁防击穿。
3. 分布式事务怎么选
| 方案 | 一致性 | 侵入性 | 适用场景 |
|---|---|---|---|
| 2PC / XA | 强一致 | 低 | 数据库层面、并发要求不高 |
| TCC | 较强,最终一致 | 高,需写 Try、Confirm、Cancel | 资金、库存等强业务一致场景 |
| 本地消息表 / 事务消息 | 最终一致 | 中 | 下单后通知、扣积分等异步场景 |
| Saga | 最终一致 | 中,需写补偿逻辑 | 长流程、跨多个服务 |
TCC 要特别注意空回滚、悬挂、幂等三个问题。选型的依据是:业务能接受多久的不一致,补偿动作是否可行。
4. 可观测性三件套
- 日志:记录发生了什么,要带上链路 ID,便于串联。
- 链路追踪:看一个请求经过了哪些服务、各耗时多少,定位慢点。
- 指标:QPS、错误率、延迟分位数(P95、P99)、资源使用率,用于告警。
排障顺序:先看指标定位异常服务,再看链路定位慢节点,最后查日志找原因。埋点和采样要平衡成本:全量采集开销大,可对错误和慢请求全采,对正常请求按比例采样。
5. 压测与性能优化
- 先压测找瓶颈(CPU、内存、IO、数据库、锁),而不是先加机器。
- 优化顺序一般是:减少不必要的请求与调用、加缓存、优化 SQL 与索引、异步化、优化算法与数据结构,最后才是扩容。
- 关注成本:单位请求的资源消耗,以及优化带来的收益。
6. 安全
- 注入:SQL 注入用参数化查询;提示词注入要隔离不可信内容并限制模型的权限。
- 越权:每次访问都校验身份与资源归属,避免只靠前端控制。
- 密钥:不写进代码库,使用密钥管理服务,定期轮换。
- 脱敏:日志、提示词、测试数据中的手机号、证件号等敏感信息要脱敏,防止泄露。
面试追问
- 你们如何保证订单与库存的一致性?(说明所选方案、失败时的补偿、对账兜底。)
- 如何做灰度发布?(按用户或流量比例放量,监控关键指标,异常时快速回滚。)
金丹:系统设计实战
系统设计题没有标准答案,面试官看的是你的思路是否有层次、取舍是否有依据。按下面五步走,再用两道经典题练手。
五步解题法
- 澄清需求:核心功能是什么?日活多少?读写比例?一致性要求?延迟要求?
- 粗算容量:日活乘人均请求数除以 86400 可估平均 QPS;峰值系数要结合业务流量曲线、活动情况或历史数据确定,缺少数据时可先做区间估算并明确假设。再估存储(单条大小乘总量乘保留时长)与带宽。
- 画整体架构:接入层(网关、负载均衡)、服务层、缓存、数据库、消息队列。
- 深入关键点:数据怎么分片、热点怎么处理、缓存怎么保持一致、写入怎么削峰、失败怎么重试。
- 谈取舍与演进:当前方案的缺点,流量增长 10 倍后哪里先出问题,如何演进。
案例一:秒杀系统
目标:瞬时高并发抢少量商品,不超卖、不拖垮系统。
口诀“拦、削、缓、扣、异”对应的做法:
- 拦:越靠前拦截越好。静态资源走 CDN;按钮防重复点击;网关限流;用户级限频;风控拦截黄牛。
- 削:请求先进入队列或令牌机制,用户排队,后端按能力处理。
- 缓:商品信息、库存预热到 Redis,读请求不打数据库。
- 扣:在 Redis 中用 Lua 脚本原子地“判断库存并扣减”,避免超卖;成功后才进入下单流程。
- 异:扣减成功后发消息,异步创建订单、落库;订单消费端做幂等。
关键细节:
- 超卖防护:Redis 预扣减加数据库层的条件更新(
update stock set n = n - 1 where id = ? and n > 0)双保险。 - 未支付订单:设置超时,到期取消并回补库存。
- 对账:定期对比 Redis 与数据库的库存,发现差异后修正。
- 降级:高峰时关闭非核心功能(评论、推荐)。
案例二:短链接服务
目标:把长 URL 转成短码,访问短码时跳转到原地址。读远多于写。
- 短码生成:用全局唯一 ID(雪花算法或号段)转 62 进制,7 位可表示约 3.5 万亿个。不要用随机数直接截取,冲突不可控。
- 存储:键为短码,值为长链接与过期时间。数据量大时按短码哈希分库分表。
- 读路径:先查本地缓存,再查 Redis,最后查数据库;热点短码缓存命中率高。
- 跳转方式:301(永久,浏览器会缓存,减轻服务压力,但统计不准)与 302(临时,每次经过服务,便于统计)。需要统计点击就选 302。
- 防护:布隆过滤器挡掉不存在的短码(防穿透);对恶意批量生成做限流。
其他练习题:计数器、排行榜、消息推送、分布式 ID 生成、评论系统。每题练到 15 分钟内讲清五步。
面试追问
- 流量再涨 10 倍,最先成为瓶颈的是什么?(通常是数据库写入或热点 key,说明你会如何分片、加缓存或拆热点。)
- 如何保证不超卖又不卖少?(预扣加超时回补加对账。)
元婴:项目表达与技术视野
到了这个阶段,面试官想知道你能否独立负责一块业务,并把技术决策讲出价值。
1. 用 STAR 讲项目
| 步骤 | 要说什么 | 示例 |
|---|---|---|
| 场景(Situation) | 业务背景与问题 | 大促期间下单接口超时率上升 |
| 任务(Task) | 你负责什么,目标是什么 | 把 P99 延迟降到 300ms 以内 |
| 行动(Action) | 你做了哪些关键决策与动作 | 引入多级缓存、拆分慢 SQL、异步化通知 |
| 结果(Result) | 用数字说明收益 | P99 从 1.2s 降到 260ms,错误率下降到 0.1% |
示例中的数字仅为格式演示,请替换成你自己的真实数据。没有量化指标的项目,说服力会大打折扣。
2. 谈风险与取舍
对每个方案准备三句话:为什么选它;放弃了什么;上线后有哪些风险,怎么兜底。例如“选事务消息而不是 TCC,是因为业务能接受秒级延迟,且改造成本低;放弃了强一致;风险是消息重复,所以消费端做了幂等并加了对账”。
3. 讲“踩坑与沉淀”
遇到的坑要讲:现象、定位过程、根因、修复、后续如何防止再发生(监控、规范、工具)。面试官最看重的是排查思路和复盘能力。
4. 技术选型的方法
- 先写出业务约束:规模、团队技能、成本、交付时间。
- 列出候选方案,对比功能、性能、运维成本、社区与风险。
- 小范围验证(PoC 或灰度),再推广。
- 不盲目追新,成熟稳定、团队能驾驭的方案往往更合适。
5. 讲 AI 赋能工程
- 适合用 AI 的场景:脚本与样板代码生成、测试用例补全、日志与错误分析、文档整理、代码评审辅助。
- 必须保留的环节:人工评审、测试验证、安全与合规检查。AI 生成的代码要当作“来自新同事的提交”来对待。
- 注意事项:不把密钥和敏感数据发给外部模型;对产出的质量与效率做度量,说明节省了多少时间、发现了多少问题。
6. 团队与规划
- 技术规划:围绕业务目标拆分三年路线,按“收益、成本、风险”排序,给技术债留出固定的偿还时间。
- 团队建设:建立代码评审与知识分享机制,让经验沉淀成文档和工具,而不是只留在个人脑中。
- 事故复盘:对事不对人,输出时间线、根因、改进项和负责人,并跟踪闭环。
面试追问
- 项目中你最大的技术挑战是什么?(选一个有取舍、有数据的例子。)
- 如果重做一遍,你会改什么?(体现反思与成长,而不是说“没有问题”。)
三、学习计划与自测题
八周学习计划
| 周次 | 主题 | 产出 |
|---|---|---|
| 第 1 周 | Java 基础、JVM、集合 | 手写 HashMap 的 put 流程,讲清扩容 |
| 第 2 周 | 并发、线程池 | 写出带完整参数的线程池,讲清死锁排查 |
| 第 3 周 | Spring、事务 | 复现并修复三种事务失效场景 |
| 第 4 周 | MySQL | 用 explain 优化 3 条慢 SQL |
| 第 5 周 | Redis、MQ | 实现缓存防击穿与消息幂等消费 |
| 第 6 周 | 分布式、AI 应用与 RAG | 搭一个最小 RAG,并做一次效果评测 |
| 第 7 周 | 系统设计 | 完整讲两道设计题,各 15 分钟 |
| 第 8 周 | 项目表达、模拟面试 | 用 STAR 写好 2 个项目,做 2 次模拟面试 |
自测题
每题限时 2 分钟口头作答,答完对照本文。
Integer为什么 128 之后==为 false?- volatile 能不能保证
count++线程安全?为什么? - HashMap 什么时候扩容、什么时候树化?
- @Transactional 失效有哪些场景?请至少说 5 种。
- 联合索引
(a, b, c),查询where a = 1 and c = 3用到了哪些列? - MySQL 为什么默认可重复读?它能否完全避免幻读?
- 缓存穿透、击穿、雪崩的区别与对策。
- 如何保证消息不丢失、不重复消费?
- 分布式事务的 TCC、消息、Saga 如何选?
- RAG 里漏检和噪声多分别怎么优化?
- 秒杀系统如何防止超卖?
- 讲一个你踩过的坑:现象、根因、修复、沉淀。
学习建议
- 每句口诀配三个“为什么”:例如 HashMap 为什么树化阈值是 8,为什么容量是 2 的幂。
- 每个知识点配一个项目场景:能把原理落到自己做过的事上,才算真正掌握。
- 每周复盘:挑一个主题自己讲一遍,讲不顺的地方就是薄弱点。
愿你修行顺利,面试一路飞升。
← 回到文章