CORE PRINCIPLES / 高频核心原理速查

核心原理速查

聚焦头部大厂仍在深入考察的核心原理,精炼四层结构速查导向

核心原理是领域切片;框在 方法论挂网。

题目数:74 | 高级 ×40 | 专家 ×3 | 架构 ×31 | 预计复习:170 分钟

一、Java 基础与集合

本组知识链

本组从语言与集合地基走到原生镜像边界:Java 21 能力面 → HashMap 并发语义 → 设计模式落点 → GraalVM/AOT 封闭世界约束。

  1. 语言能力面:Virtual Threads、Pattern Matching、分代 ZGC、Sequenced Collections——先弄清适用边界再换编程模型。
  2. 集合与哈希:扩容、扰动、树化与并发死链解释「看似线程安全容器」的真实成本。
  3. 设计模式:策略/工厂/观察者/责任链/模板方法对应扩展点,而不是类图背诵。
  4. Native Image:封闭世界假设下反射/代理/资源必须配置化;与 Spring Boot 3 AOT 同一条迁移链。
工程化要点
  • JDK 大版本与预览 API 变更单独验收,预览能力不得绑核心路径。
  • HashMap/CHM 选型写入并发评审:单线程可变、并发读多写少、极端争用分开论证。
  • 集合扩容与树化阈值纳入容量画像,避免高峰触发 rehash 抖动。
  • 设计模式落点要对齐扩展点与测试替身,禁止为模式而模式。
  • GraalVM Native 构建配置(reflect/resource/proxy)进 CI,回归覆盖启动与反射路径。
  • AOT/CDS/Leyden 启动优化与功能回归矩阵同变更单。
  • 集合与流式 API 的分配画像进压测基线,防止「优雅写法」制造分配风暴。
本组思维模式
  • 边界先于语法糖:新特性先问适用场景与回退,再谈写法漂亮。
  • 容器语义:线程安全不等于无争用;先画访问模式再选结构。
  • 扩展点意识:模式是变化隔离手段,验收看替换成本。
  • 封闭世界:动态性是 Native 的一等成本,配置即契约。
  • 迁移当变更:语言/运行时升级按生产变更管控。
E01.01
Java 21 新特性:Virtual Threads、Pattern Matching、ZGC Generational、Sequenced Collections高级P1
核心要点
  • Virtual Threads(JEP 444):JVM 级轻量级线程,由 JVM 调度而非 OS,单个线程栈初始仅几 KB(按需扩展),可创建数百万级
  • Structured Concurrency(JEP 453 起多次预览,最新 JEP 499/505):结构化并发 API,将多个子任务视为一个工作单元,失败/超时自动取消所有子任务
  • Pattern Matching for switch(JEP 441):switch 支持模式匹配与 null 分支,替代冗长的 if-else instanceof 链
  • ZGC Generational(JEP 439):ZGC 支持分代回收,新生代与老年代独立收集,吞吐量提升 10%~20%
  • Sequenced Collections(JEP 431):新增 SequencedCollection/SequencedSet/SequencedMap 接口,统一首尾元素操作
详细解答

Virtual Threads 是 Java 21 最核心的特性。与传统 OS 线程(Platform Thread)1:1 映射不同,Virtual Thread 由 JVM 的 ForkJoinPool 调度,挂载(mount)到平台线程上执行,遇到阻塞 IO 时自动卸载(unmount),平台线程可继续执行其他虚拟线程。这使得高并发场景下线程创建成本从 MB 级降到 KB 级。

但需注意 pinning 问题:虚拟线程在 synchronized(JDK 21–23)或 native 方法中阻塞时可能无法 unmount。诊断按版本:JDK 21–23 可用 -Djdk.tracePinnedThreads=full|short;JDK 24 起该系统属性已移除,改用 JFR jdk.VirtualThreadPinned 与新式线程转储。JDK 24+(JEP 491)几乎消除 synchronized pinning,剩余重心在 native/JNI。完整对照见 C04.09 / E03.06。

Pattern Matching for switch 允许 switch 表达式直接对引用类型进行模式匹配:

switch (obj) {
    case Integer i -> System.out.println("int " + i);
    case String s  -> System.out.println("str " + s);
    case null      -> System.out.println("null");
    default        -> System.out.println("other");
}

ZGC Generational 将堆划分为年轻代和老年代,年轻代使用标记-复制算法,暂停时间通常保持在 10ms 以内(部分场景低于 1ms),同时吞吐量显著优于单代 ZGC,适合大堆低延迟场景。

工程实践

Spring Boot 3.2+ 开启虚拟线程:spring.threads.virtual.enabled=true。压测用 jcmd <pid> Thread.dump_to_file;pinning 诊断按 JDK 版本选择工具(见上),勿在 JDK 24+ 仍依赖 tracePinnedThreads。Structured Concurrency 至较新 JDK 仍多为 Preview,勿绑核心生产路径。

高频追问
  • Q: Virtual Thread 与协程(Kotlin Coroutine/Go Goroutine)的本质区别?A: Virtual Thread 是 JVM 线程抽象,仍使用 Thread API;协程是语言级概念,需要 suspend/await 显式协作。Virtual Thread 的阻塞点由 JVM 自动处理,无需修改代码。
  • Q: ZGC Generational 是否还需要 G1?A: JDK 21 中 G1 仍是默认收集器,ZGC Generational 适合 >100GB 大堆且延迟敏感的场景。一般业务系统 G1 足够。
Java 21 Virtual Threads ZGC Pattern Matching
E01.02
HashMap 底层:扩容机制、hash 扰动、链表转红黑树、并发死链高级P0
核心要点
  • 底层数组+链表/红黑树,数组长度恒为 2 的幂,默认初始容量 16,负载因子 0.75
  • hash 扰动:(h = key.hashCode()) ^ (h >>> 16),高 16 位与低 16 位异或,减少低位冲突
  • 扩容阈值 = 容量 × 负载因子;扩容时容量翻倍,元素重新散列到新数组
  • 链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树;树节点 < 6 时退化为链表
  • JDK 1.8 之前并发扩容存在死链问题(头插法导致循环链表),1.8 改为尾插法但仍有数据丢失风险
详细解答

HashMap 通过 (n - 1) & hash 计算下标,数组长度 n 为 2 的幂时,等价于 hash % n 且效率更高。hash 扰动函数将高 16 位与低 16 位异或,是因为下标计算只用到低几位,高位信息被浪费,扰动后高位也参与运算,减少碰撞。

扩容(resize)是 HashMap 最耗时的操作。当元素数量超过阈值时,容量翻倍为 2n,所有元素重新计算下标。JDK 1.8 优化:如果原下标为 i,扩容后要么仍在 i,要么在 i + n(因为容量翻倍后最高位多一位,hash 对应位为 0 则不变,为 1 则加 n)。

链表转红黑树的阈值 8 来源于泊松分布:当负载因子为 0.75 时,链表平均长度为 0.5,长度达到 8 的概率已低于百万分之一。退树阈值 6 而非 8 是为了防止频繁在链表与树之间震荡。

并发安全问题:JDK 1.7 头插法扩容时,两个线程同时操作可能导致环形链表,后续 get 会死循环。JDK 1.8 改为尾插法避免了死链,但并发 put 仍可能丢失数据(覆盖写入)。

关键机制深解:树化与退化的全链路

TreeNode 的双重结构。HashMap 桶内树化后的节点是 TreeNode extends LinkedHashMap.Entry,除了红黑树所需的 parent/left/right/red 指针,它还通过继承自 Entry 的 next/prev 维持一条双向链表。也就是说,一个桶同时持有「红黑树」与「双向链表」两套结构:查找走树(O(log n)),containsValue / 迭代器顺序遍历走链表。树只在查找路径上重组,链表顺序保留,因此树化后迭代顺序依然稳定。

树化的双闸门。链表转红黑树必须同时满足:① 桶内链表长度 ≥ TREEIFY_THRESHOLD(8);② 表数组容量 ≥ MIN_TREEIFY_CAPACITY(64)。若长度到了 8 但容量还 < 64,HashMap 优先选择 resize() 而非树化——因为小表扩容后单桶元素骤减,往往根本无需树化;树化本身有构建成本,先扩容性价比更高。

treeify / untreeify 的全过程。treeifyBin() 先把普通 Node 逐个替换为 TreeNode,再对每个节点做 balanceInsertion(红黑插入平衡:叔节点红则变色、黑则旋转),最后用 moveRootToFront 把树根放到桶首。untreeify() 则沿 prev/next 把 TreeNode 还原成普通 Node 链表。退化阈值 UNTREEIFY_THRESHOLD=6 与树化 8 之间留了 2 的迟滞(hysteresis),专门防止在 6~8 区间内因增删反复「树化↔退化」震荡。

扩容时的 split。树桶扩容不会整体重排,而是按哈希高位把节点 split 成 lo(低位)和 hi(高位)两棵子树;若某一侧子树节点数 ≤ 6,直接 untreeify 退化为链表,否则保留 TreeNode 并可能再平衡。这样大红黑树在扩容时只做「拆分 + 局部平衡」,避免 O(n) 重排。

红黑性质与碰撞攻击。红黑树保证最长路径 ≤ 2×最短路径、查找稳定 O(log n);balanceInsertion 在插入后修复黑高一致。恶意方构造大量 hashCode 碰撞的 key 能把所有元素压进同一桶并触发树化——树化把最坏 O(n) 降到 O(log n),但树化本身耗 CPU,且查询成本仍高于均匀分布。根本防御不在 HashMap(扰动函数只是缓解),而在「不可控 key 的 hashCode 不可预测」+ 限流 + 拒绝超大单桶请求。

工程实践

已知数据量时指定初始容量:new HashMap<>(expectedSize / 0.75F + 1.0F),避免扩容。高并发读多写少用 Collections.unmodifiableMap 包装,或换用 ConcurrentHashMap。

高频追问
  • Q: 为什么 HashMap 不用平衡二叉树而用红黑树?A: 红黑树插入/删除旋转次数更少,实现更简单,平均性能与 AVL 树接近但常数更优。HashMap 主要场景是查找,红黑树足够。
  • Q: 为什么负载因子是 0.75 而不是 0.5 或 1.0?A: 0.75 是时间与空间的权衡:0.5 浪费空间但减少碰撞;1.0 节省空间但链表/树化概率大增。0.75 在泊松分布下链表平均长度可控。
  • Q: 为什么容量 ≥ 64 才树化,否则先扩容?A: 小表扩容后单桶元素大幅减少,多数情况下无需树化;树化有构建与平衡成本,先扩容(翻倍 + 重新散列)的边际收益更高。MIN_TREEIFY_CAPACITY=64 是实测经验阈值。
  • Q: TreeNode 为什么还要保留链表结构?A: 三层原因——① 扩容 split 时需要沿链表顺序拆分(树指针在 resize 时不稳定,先退化为链表再分发);② containsValue / 迭代器需要顺序遍历,链表比树中序遍历更直接;③ 保证迭代器在树重组时仍输出稳定顺序。
HashMap 红黑树 并发安全 扩容
E01.03
设计模式全景:GoF 23 种与 Spring 框架落地(意图·Spring 落点·陷阱)高级P1
核心要点
  • 策略模式(Strategy):定义算法族,分别封装,让它们可互相替换。Spring 的 Resource、Comparator 都是策略模式。适合消除大量 if-else 分支
  • 工厂模式:简单工厂(一个工厂方法创建多种产品)、工厂方法(每个产品对应一个工厂类)、抽象工厂(产品族)。Spring 的 BeanFactory 是工厂模式的典型应用
  • 观察者模式(Observer):定义对象间的一对多依赖,当一个对象状态改变时,所有依赖者自动收到通知。JDK 的 Observable/Observer(JDK 9 起已废弃,推荐 Flow API / Project Reactor 替代)、Spring 的 ApplicationEvent、消息队列都是观察者扩展
  • 责任链模式(Chain of Responsibility):将请求沿处理链传递,每个处理器决定是否处理或转交。Servlet Filter、Dubbo Filter、Sentinel SlotChain 均使用此模式
  • 模板方法模式(Template Method):定义算法骨架,子类重写特定步骤。Spring 的 JdbcTemplate、RestTemplate、AbstractController 都是模板方法的应用
详细解答

策略模式实现:定义 Strategy 接口,多个实现类(ConcreteStrategy),Context 持有 Strategy 引用并在运行时注入。结合 Spring 可通过 @Autowired List<Strategy> 自动收集所有实现,配合 Map 或工厂快速定位。相比 if-else,策略模式符合开闭原则,新增策略无需修改原有代码。

观察者模式与发布订阅的区别:观察者模式中观察者和被观察者直接耦合(如 ApplicationListener 和 ApplicationEventPublisher);发布订阅模式引入事件中心(Broker),发布者和订阅者完全解耦(如 Kafka/RabbitMQ)。

责任链模式与装饰器模式的区别:责任链中每个节点有机会终止链或继续传递;装饰器模式每个装饰者都一定会调用被装饰者,目的是增强功能而非决策。两者都使用链式结构,但意图不同。

模板方法与回调的区别:模板方法通过继承实现,骨架在父类,子类重写方法;回调通过组合/接口实现,调用方传入一个回调对象(如 Runnable/Callable/Consumer)。回调更灵活,避免了继承的强耦合。

工程实践

设计模式使用原则:① 不要过度设计,简单 if-else 在分支少时更清晰;② 优先组合优于继承(策略/装饰器/回调);③ 模式是沟通语言,命名时体现模式意图(如 XxxStrategy、XxxFactory)。Spring 框架本身就是设计模式大全:IoC/DI(工厂+依赖注入)、AOP(代理+装饰器)、@EventListener(观察者)。

高频追问
  • Q: 单例模式在 Spring 中如何体现?A: Spring 默认 Bean 作用域为 singleton,通过 IoC 容器保证单例。但非懒加载单例在分布式/多 ClassLoader 下不是全局唯一。JDK 枚举是实现单例的最佳方式,天然防反射和序列化破坏。
  • Q: 代理模式与装饰器模式的区别?A: 代理模式控制访问(如权限检查、延迟加载),装饰器模式增强功能。实现上两者都持有一个目标对象引用,但意图不同。Spring AOP 是动态代理(JDK/CGLIB)的应用。
扩展补全:设计模式深度细化

不仅要求掌握高频模式,还需要对 GoF 23 种设计模式有整体认知,能快速说出分类、Spring 中的应用位置,并能针对细节模式展开深度解析。以下从分类矩阵、深度解析、选型决策树与反模式四个维度补齐。

分类 模式 核心意图 Spring 框架应用
创建型 单例(Singleton) 确保全局唯一实例 Bean 默认 singleton 作用域
创建型 工厂方法(Factory Method) 子类决定创建哪种产品 BeanFactory.getBean()
创建型 抽象工厂(Abstract Factory) 创建产品族 BeanFactory/ApplicationContext
创建型 建造者(Builder) 分步构建复杂对象 BeanDefinitionBuilder、StringBuilder
创建型 原型(Prototype) 克隆已有实例 @Scope("prototype")
结构型 适配器(Adapter) 接口转换 Spring MVC HandlerAdapter
结构型 桥接(Bridge) 抽象与实现分离 SPI 机制(DriverManager)
结构型 组合(Composite) 树形结构统一处理 CompositeCacheKey
结构型 装饰器(Decorator) 动态增强 HttpServletRequestWrapper
结构型 外观(Facade) 简化子系统接口 JdbcTemplate/RestTemplate
结构型 享元(Flyweight) 共享细粒度对象 Integer Cache、String Pool
结构型 代理(Proxy) 控制访问 AOP 动态代理(JDK/CGLIB)
行为型 责任链(Chain of Responsibility) 链式处理 Filter/Interceptor/HandlerExecutionChain
行为型 命令(Command) 封装请求为对象 JdbcTemplate.StatementCallback
行为型 解释器(Interpreter) 定义文法解释 SpEL(Spring Expression Language)
行为型 迭代器(Iterator) 顺序访问集合 CompositeIterator
行为型 中介者(Mediator) 集中管理交互 ApplicationContext 作为中介者
行为型 备忘录(Memento) 保存/恢复状态 StateManager(Spring StateMachine)
行为型 观察者(Observer) 一对多通知 ApplicationEvent/EventListener
行为型 状态(State) 状态驱动行为 Spring StateMachine
行为型 策略(Strategy) 算法族可替换 Resource 接口多实现
行为型 模板方法(Template Method) 骨架+子类填充 JdbcTemplate/RestTemplate
行为型 访问者(Visitor) 操作与结构分离 BeanDefinitionVisitor

下面补充除核心 5 个模式之外常被追问的模式深度解析。

  • 装饰器模式:与继承的区别在于——装饰器是运行时组合,继承是编译时固定。经典场景:Java IO 流(InputStream → FileInputStream → BufferedInputStream → DataInputStream)。陷阱:多层装饰导致调试困难(调用栈深),建议限制装饰层数 ≤ 3。Spring 中 HttpServletRequestWrapper 是装饰器,可在 Filter 中增强 Request。
  • 适配器模式:将不兼容接口转为兼容接口。与装饰器的区别:装饰器增强已有功能,适配器转换接口(不改功能)。Spring MVC 的 HandlerAdapter 适配不同类型的 Controller(SimpleControllerHandlerAdapter 适配 Controller 接口,RequestMappingHandlerAdapter 适配 @RequestMapping 注解)。陷阱:适配器过多表明接口设计有问题,应重构接口而非加适配器。
  • 外观模式:为复杂子系统提供简化入口。JdbcTemplate 是外观模式——封装了 Connection/Statement/ResultSet 的创建、异常转换、资源释放,开发者只需提供 SQL 和 RowMapper。陷阱:外观不是上帝类,外观只做"转发"不做"决策",避免外观类膨胀为巨石类。
  • 代理模式:控制对象访问(延迟加载、权限检查、远程调用)。三种代理:静态代理(编译期生成)、JDK 动态代理(基于接口,Proxy.newProxyInstance)、CGLIB 动态代理(基于继承,Enhancer.create)。Spring AOP 的代理选择:有接口用 JDK 代理,无接口用 CGLIB;Spring Boot 2.x+ 默认强制 CGLIB。陷阱:CGLIB 代理无法代理 final 类/方法;JDK 代理目标类必须实现接口。
  • 状态模式:对象行为随内部状态变化。与策略模式的区别:策略模式客户端选择策略(外部控制),状态模式对象内部自动切换状态(内部驱动)。场景:订单状态机(待支付→已支付→已发货→已完成/已取消),Spring StateMachine 是状态模式的工程化框架。陷阱:状态类过多导致类爆炸,简单状态机用枚举+switch 更清晰。
  • 命令模式:将请求封装为对象(命令对象包含接收者+动作+参数)。场景:Runnable 是命令模式的简化版(只有动作,无接收者和参数)。Spring JdbcTemplate.execute(ConnectionCallback action) 将 Connection 操作封装为 ConnectionCallback 命令对象。陷阱:命令模式引入额外类层级,简单调用不值得封装。
  • 建造者模式:分步构建复杂对象。经典场景:StringBuilder、Stream.Builder、Lombok @Builder。与工厂模式的区别:工厂一次性创建完整对象,建造者分步构建(可跳过某些可选参数)。陷阱:建造者适合参数多(>4)且大多可选的场景;参数少时直接构造器更清晰。
  • 组合模式:将对象组合成树形结构,统一处理单个对象和组合对象。场景:Spring Security 的 CompositeFilterChainProxy、UI 组件树(Vue/React 组件嵌套)。陷阱:组合模式中"叶子节点"和"组合节点"实现同一接口,叶子节点的 add/remove 方法应抛出 UnsupportedOperationException。

其余 15 种模式深解(意图 + Spring 落点 + 关键陷阱,补全 GoF 全谱):

  • 单例(Singleton):保证进程内唯一实例。Spring 默认 singleton Bean 由容器缓存保证;但多 ClassLoader/JVM 下非全局唯一。JDK 枚举是最佳实现(防反射/序列化破坏)。陷阱:懒加载双重检查锁必须用 volatile 修饰实例(防指令重排导致半初始化对象逃逸);Spring 的单例是「容器级」非「JVM 级」。
  • 工厂方法(Factory Method):子类决定产品。Spring BeanFactory.getBean() 按类型/名称获取,AbstractBeanFactory 是工厂方法骨架;FactoryBean 更特殊——它本身是工厂,getObject() 返回产品(如 SqlSessionFactoryBean)。陷阱:FactoryBean 与 BeanFactory 易混;& 前缀取工厂本身。
  • 抽象工厂(Abstract Factory):创建产品族(一组相关产品)。Spring ApplicationContext 是抽象工厂——同时生产 Bean、Environment、ResourceLoader、MessageSource 等一族对象。陷阱:产品族扩展容易,但新增产品等级结构需改接口(违反开闭),适合「家族稳定、个体多变」。
  • 原型(Prototype):每次获取克隆新实例。Spring @Scope("prototype") 每次 getBean 新建;JDK clone() 是原型应用,但有浅拷贝陷阱(引用字段共享)。陷阱:prototype 依赖注入只在创建时进行一次,容器不管理其销毁;循环依赖中 prototype 无法解决(抛 BeanCurrentlyInCreationException)。
  • 桥接(Bridge):抽象与实现分离,各自独立变化。经典:JDBC 的 Driver(实现侧)与 Connection/Statement(抽象侧)经 DriverManager 桥接,切换驱动不改业务;Spring PlatformTransactionManager 桥接不同事务实现(JDBC/JTA/Redis)。陷阱:桥接 vs 适配器——桥接在「设计期」分离抽象与实现,适配器在「运行期」接已存在的不兼容接口。
  • 享元(Flyweight):共享细粒度对象降内存。JDK:Integer.valueOf 缓存 -128~127、Boolean/String 常量池、Charset 缓存;连接池(HikariCP 复用 Connection)、线程池(复用 Thread)本质都是享元。陷阱:享元对象必须不可变(内部状态共享),可变状态必须外置为「外部状态」由调用方传入,否则并发串数据。
  • 迭代器(Iterator):统一遍历集合,隔离内部结构。JDK Iterable/Iterator 的 fail-fast(遍历中结构变更抛 ConcurrentModificationException);Spring CompositeIterator 组合多迭代器。陷阱:fail-fast 非 fail-safe,并发遍历用 CopyOnWrite/Concurrent 集合或显式加锁;remove() 必须走 Iterator 而非集合自身。
  • 中介者(Mediator):用中介对象封装一组对象交互降耦合。Spring ApplicationContext 是巨型中介者——Bean 间不直接引用,全靠容器牵线;MVC DispatcherServlet 也是中介者(集中调度 Handler/View/Resolver)。陷阱:中介者易膨胀成「上帝类」,复杂交互应拆子中介或结合观察者。
  • 备忘录(Memento):捕获并恢复内部状态,不破坏封装。JDK Serializable 快照;Spring StateMachine 的 StateMachineContext 充当备忘录保存状态机快照(持久化恢复)。陷阱:备忘录持有完整状态,频繁保存有大内存/序列化成本;只存「必要最小状态」,结合原型做深拷贝。
  • 观察者(Observer):一对多依赖,状态变更自动通知。JDK 旧 Observable/Observer(JDK9 废弃)→ Flow API/Reactor 替代;Spring ApplicationEvent + @EventListener(同步/异步可选、泛型事件);MQ(Kafka/RabbitMQ)是跨进程发布订阅。陷阱:观察者链过长致通知延迟;同步观察者抛异常会中断后续(Spring 默认同步,需 @Async 或 ErrorHandler);避免观察者间循环依赖。
  • 责任链(Chain of Responsibility):请求沿链传递,节点决定处理或转发。Servlet Filter、Spring HandlerInterceptor、Dubbo Filter、Sentinel SlotChain、MyBatis Plugin。陷阱:链过长增加 RT;某节点吞异常或不调用 chain 会「断链」;顺序敏感(鉴权必须在业务前)。
  • 解释器(Interpreter):给定文法定义解释器。Spring 的 SpEL(Spring Expression Language)是解释器模式——把表达式解析为 AST 后求值(@Value、安全表达式、权限注解底层)。陷阱:复杂文法用解释器性能差、类膨胀;适合「规则/表达式频繁变、结构稳定」,重计算交专用引擎。
  • 策略(Strategy):算法族可替换,消除 if-else。Spring Resource 接口多实现(ClassPath/FileSystem/Url/InputStream),ResourceLoader 按前缀选策略;@Autowired List<Strategy> 自动收集全部实现做路由。陷阱:策略类过多时用 Map 缓存路由(key=类型,value=实现)避免遍历;策略与工厂常配合(工厂创建、策略行为)。
  • 模板方法(Template Method):父类定骨架,子类填步骤。JDK AbstractList/InputStream;Spring JdbcTemplate/RestTemplate/HibernateTemplate(固定「资源获取→执行→异常转换→释放」,钩子由回调传)。陷阱:父类抽象方法过多增子类负担;能用回调(组合)替代继承时优先回调(避免强耦合、更灵活)。
  • 访问者(Visitor):将操作与数据结构分离,便于新增操作。JDK FileVisitor(遍历目录树执行 visitFile/visitDir);Spring BeanDefinitionVisitor 遍历 BeanDefinition 做属性占位符替换。陷阱:访问者依赖「元素类稳定」(新增元素类型要改所有 Visitor),适合「结构稳定、操作多变」;双分派(element.accept(visitor) → visitor.visit(element))是核心机制。

模式选型决策树——按需求维度快速定位:

  • 需要创建对象?→ 创建型
    • 一步创建 → 工厂/抽象工厂
    • 分步构建 → 建造者
    • 克隆已有 → 原型
    • 全局唯一 → 单例
  • 需要组合对象?→ 结构型
    • 增强功能 → 装饰器
    • 转换接口 → 适配器
    • 简化入口 → 外观
    • 控制访问 → 代理
    • 树形结构 → 组合
  • 需要对象交互?→ 行为型
    • 一对多通知 → 观察者
    • 链式处理 → 责任链
    • 算法替换 → 策略
    • 状态驱动 → 状态
    • 算法骨架 → 模板方法

反模式:过度设计。过度设计信号:为只有 2 个分支的逻辑引入策略模式、为简单 CRUD 引入工厂+建造者、为内部调用引入代理。判断标准:模式带来的收益(扩展性/可读性)是否超过其成本(类数量增加、间接层增加、理解成本上升)。重构时机:当 if-else 超过 4 分支且每分支逻辑超过 10 行时,考虑策略模式;否则保持简单。经典反例:一个只有 3 个字段的 DTO 用 Builder 构建(直接 new + setter 更清晰)。

设计模式 策略模式 工厂模式 责任链
E01.04
GraalVM Native Image 兼容性:封闭世界假设、反射/动态代理/资源治理、构建配置与 Spring Boot 3 AOT 适配架构P2
核心要点
  • Native Image 原理:AOT 编译,Closed World Assumption(封闭世界假设),构建期完成可达性分析,运行时无 JIT、无类加载器动态加载
  • 四大兼容性挑战:反射(运行时类元数据缺失)、动态代理(CGLIB/ByteBuddy 字节码生成受限)、资源文件(默认不打包)、JNI/第三方 native 库(需重新链接)
  • 构建配置三件套:reachability-metadata.json(反射/资源元数据)、native-image.properties(构建参数)、@RegisterForReflection(注解声明)
  • Spring Boot 3 适配:3.2+ 内置 GraalVM Native Build Tools,AOT 模式在构建期生成 metadata,运行时反射调用降级为直接调用
  • 启动 vs 峰值:Native 启动 ~50ms(vs JVM 2~5s),内存占用低 50%+;但峰值吞吐低 10%~30%(无 JIT 运行时优化)
详细解答

封闭世界假设:构建期静态分析所有可达类,未被引用的类直接剔除;运行时 Class.forName("X") 加载未注册类会失败。这是 Native Image 与 JVM 的根本分歧。

反射治理:Hibernate/Jackson/Gson 大量用反射,需通过 reachability-metadata.json 显式注册类与字段;Spring AOT 在构建期把 @Bean、@Configuration 等反射调用前移到构建期生成代码,运行时不再反射。MyBatis 等动态 SQL 框架需验证适配情况。

动态代理:JDK Proxy 需注册被代理接口(构建期生成代理类);CGLIB/ByteBuddy 字节码运行时生成在 Native 下基本不可用,Spring 已将 AOP 代理生成前移到 AOT 阶段。第三方库若仍用运行时字节码增强(如旧版 Hibernate 动态模型)需改造。

资源文件:src/main/resources 下配置默认不打包,需在 native-image.properties 用 -H:IncludeResources=.*\.properties 显式声明;Spring Boot 通过 native-image-resource-pattern 自动处理。

性能权衡:Native 适合短生命周期/冷启动敏感场景(FaaS、K8s Job、CLI);长运行服务失去 JIT 优化收益,峰值吞吐低 10%~30%,需 PGO(Profile-Guided Optimization)弥补。

工程实践

构建命令:./gradlew nativeCompile 或 mvn -Pnative package,需 GraalVM SDK + native-image 工具链。metadata 生成:-H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image 指定自定义 metadata;Spring Boot 用 ProcessAot/AotProcessor 自动生成。内存优化:-H:+UseCompressedReferences、-R:MaxHeapSize=512m。适用场景:AWS Lambda / K8s Job / CLI / 函数计算是最佳实践;长期服务慎用。调试:-H:+PrintAnalysisCallTree 查看可达性分析,-H:ReportExceptionFlow 排查反射缺失点。

高频追问
  • Q: Native Image 适合所有 Spring Boot 应用吗?A: 不适合。重度反射(Hibernate 动态模型)、运行时字节码增强(部分 ORM/SPI)、动态加载(OSGi/运行时 SPI 发现)的应用改造成本极高,需逐个验证第三方 starter 适配。
  • Q: Native 与 JIT 的吞吐差距能补齐吗?A: 部分可补:PGO(Profile-Guided Optimization):GraalVM 企业版早期支持,社区版自 GraalVM 21 起显著完善,可弥补 Native Image 吞吐劣势;用 G1GC 替代默认 GC;但峰值吞吐仍难追 JIT(JIT 可基于运行时 profile 做激进内联/去虚化)。
  • Q: 反射缺失为什么这么痛?A: 构建期做可达性分析,未被静态引用的类直接被剔除;运行时反射找不到 Class 对象;必须 metadata 显式告知"该类运行时会被反射访问",否则抛反射元数据缺失类异常(如 MissingReflectionRegistrationException / UnsupportedFeatureError,视 GraalVM 版本而定)。
  • Q: Spring Boot 3 对 Native 支持成熟度?A: 3.0 起原生支持,3.2+ 提供完整 @NativeHint 体系;但 MyBatis、部分 Redis 客户端、动态数据源等第三方 starter 需验证,建议先在非核心服务灰度。
GraalVM AOT Native Image Spring Boot 3

二、JVM 与内存

本组知识链

本组把 JVM 排障收成一条链:收集器能力边界 → 类加载隔离 → OOM 证据链 → ZGC 染色指针与读屏障机制。

  1. 收集器谱系:Serial→Parallel→CMS→G1→ZGC/Shenandoah,先定延迟/吞吐/足迹再选型。
  2. 类加载:双亲委派与打破委派(Tomcat/SPI/OSGi)解释同名类冲突。
  3. 调优与 OOM:参数、GC 日志、Dump、应急动作形成可复现证据。
  4. ZGC 机制:染色指针、读屏障、重定位与分代 ZGC 决定低停顿如何成立。
工程化要点
  • GC 调参必须带对照实验:日志 + 延迟直方图 + 错误率。
  • 收集器/关键参数变更走灰度与一键回滚,写入错误预算条件。
  • 类加载器泄漏纳入金丝雀观察:Metaspace 必须可回落。
  • OOM 与 OOMKilled 分诊 Runbook 进值班手册。
  • 容器下 ActiveProcessorCount、堆上限与 cgroup 三联对齐。
  • ZGC/分代 ZGC 上线前验证分配率与大对象路径。
  • 诊断工具链(jcmd 权限、只读根文件系统)上线前打通。
本组思维模式
  • 目标函数先行:先定 SLO 再选收集器,禁止先调参后补目标。
  • 分区归因:堆/元空间/直接内存/线程分清再谈手段。
  • 证据优于经验:无日志对照的调参视为违规变更。
  • 加载器边界:同名类冲突先画 ClassLoader 图。
  • 机制解释现象:低停顿靠并发标记转移,不是「换个名字就快」。
E02.01
JVM GC 算法演进:Serial → Parallel → CMS → G1 → ZGC/Shenandoah 对比架构P0
核心要点
  • Serial:单线程 STW,适合客户端和小应用;Parallel:多线程 STW,吞吐量优先,JDK 8 默认
  • CMS:并发低停顿,初始标记→并发标记→重新标记→并发清理,但内存碎片、浮动垃圾、并发模式失败(已废弃:JDK 9 标记废弃、JDK 14 移除,当前推荐 G1/ZGC/Shenandoah)
  • G1:Region 化堆(1~32MB),优先回收垃圾最多的 Region,可预测停顿时间(默认 200ms),JDK 9+ 默认
  • ZGC:染色指针(Colored Pointers)、读屏障、并发整理,STW < 10ms,支持 TB 级堆,JDK 15+ 生产可用
  • Shenandoah:与 ZGC 类似,但由 RedHat 开发,更早进入 OpenJDK,也是低延迟并发收集器
详细解答

CMS(Concurrent Mark Sweep) 的设计目标是低停顿:初始标记(STW,标记 GC Roots 直接引用)→ 并发标记(与用户线程并行遍历对象图)→ 重新标记(STW,修正并发期间的变动)→ 并发清理(回收垃圾)。但 CMS 有三个致命缺陷(注:CMS 已于 JDK 9 标记废弃、JDK 14 正式移除,以下缺陷分析仅作演进历史对比,当前生产推荐 G1/ZGC/Shenandoah):

  • 内存碎片:使用标记-清除算法,不压缩,长期运行后老年代碎片严重,可能触发 Full GC
  • 浮动垃圾:并发清理期间用户线程继续产生垃圾,需预留空间,若预留不足则 Concurrent Mode Failure,退化为 Serial Old
  • CPU 敏感:并发阶段占用 CPU 资源,CPU 核数少时吞吐量下降明显

G1(Garbage First) 将堆划分为等大小的 Region,每个 Region 可动态扮演 Eden、Survivor、Old 角色。维护优先级列表(Remembered Set + Collection Set),每次回收时选择垃圾最多的 Region,在指定停顿时间内回收最大量的垃圾。G1 的混合回收(Mixed GC)同时回收年轻代和部分老年代,避免传统 Full GC。

ZGC 的核心是染色指针和读屏障。染色指针将元信息(标记状态、重映射状态)编码在 64 位指针的高 4 位,无需对象头标记。并发整理时,读屏障拦截对象引用访问,若对象已移动则自动重定向到新地址(自愈)。ZGC 的 STW 时间极短(<10ms)且与堆大小无关,适合超大堆(TB 级)和低延迟场景。

工程实践

JDK 8 默认 Parallel GC,建议升级 JDK 17+ 使用 G1(平衡吞吐与延迟)。延迟敏感(如金融交易)用 ZGC:-XX:+UseZGC(JDK 21 需追加 -XX:+ZGenerational;JDK 23+ 已默认分代、无需该参数;该参数 JDK 24 废弃、JDK 26 起移除)。大堆(>32GB)避免 CMS,G1 或 ZGC 更合适。调优时关注 GC 日志中的 pause time 和 throughput。

高频追问
  • Q: G1 的 Remembered Set 是什么?A: 每个 Region 维护一个 RSet,记录哪些其他 Region 引用了本 Region 的对象。回收时只需扫描 RSet,无需扫描整个堆,实现 Region 独立回收。
  • Q: ZGC 的染色指针为什么不支持 32 位系统?A: 32 位指针只有 4 字节,高 4 位被染色指针用于元信息,剩余 28 位寻址空间仅 256MB,无实用价值。ZGC 仅支持 64 位。
JVM GC ZGC G1
E02.02
类加载器:双亲委派模型、打破委派(Tomcat/Spi/OSGi)高级P0
核心要点
  • 三层类加载器:Bootstrap(<JAVA_HOME>/lib)→ Extension(ext 目录,JDK 9 后移除)→ Application(classpath)
  • 双亲委派:加载类时先委托父加载器,父加载器无法加载时才自己加载,保证核心类库安全(防止自定义 java.lang.String)
  • 打破委派场景:Tomcat 隔离 Web 应用(每个应用独立类加载器)、SPI(接口由父加载器加载,实现由子加载器加载)、OSGi 模块化
  • 线程上下文类加载器(Thread Context ClassLoader):SPI 机制中,父加载器通过 TCCL 委托子加载器加载实现类
  • JDK 9 引入 Platform ClassLoader 替代 Extension ClassLoader,支持模块系统(JPMS)
详细解答

双亲委派的工作流程:类加载器收到加载请求 → 先检查是否已加载 → 未加载则委托父加载器 → 父加载器递归委托 → 顶层 Bootstrap 无法加载时,子加载器才尝试自己加载。

打破双亲委派的三种典型场景:

  1. Tomcat:每个 Web 应用有独立的 WebAppClassLoader,先自己加载 WEB-INF/classes 和 lib 下的类,加载不到才委托父加载器。这实现了应用间类隔离,避免不同版本的 Spring 冲突。
  2. SPI(Service Provider Interface):如 JDBC 的 java.sql.Driver 接口由 Bootstrap ClassLoader 加载,但具体实现(如 mysql-connector-java.jar)在 classpath 下,由 App ClassLoader 加载。父加载器无法加载子加载器的类,因此使用 Thread.currentThread().getContextClassLoader() 反向委托。
  3. OSGi:每个 Bundle 有自己的 ClassLoader,模块间通过 Import-Package/Export-Package 声明依赖,形成复杂的类加载器网络,完全打破树形委派。

JDK 9 模块系统(JPMS)引入了层(Layer)的概念,每个层有自己的类加载器,模块显式声明依赖(requires)和导出(exports),类加载更加精细化和安全。

工程实践

自定义类加载器继承 URLClassLoader,重写 findClass 而非 loadClass(保留双亲委派)。热部署场景(如插件系统)需独立类加载器 + 旧加载器卸载(无引用后可 GC)。注意 ClassLoader 泄漏:静态变量、ThreadLocal、JDBC 驱动注册等可能阻止卸载。

高频追问
  • Q: 为什么不能自定义 java.lang.String?A: 双亲委派下 String 由 Bootstrap ClassLoader 从 rt.jar 加载。即使 classpath 下有自定义 String,也会先被 Bootstrap 加载核心类。打破委派后理论上可以,但会导致核心类加载混乱,JVM 禁止。
  • Q: 同一个类被两个 ClassLoader 加载,是同一个类吗?A: 不是。JVM 判断类相同需类全限定名 + 定义类加载器都相同。两个 ClassLoader 加载的同名类属于不同 Class 对象,不能互相转型。
类加载器 双亲委派 Tomcat SPI
E02.03
JVM 调优与 OOM 排查:参数配置、GC 日志分析、内存 Dump、线上应急高级P0
核心要点
  • 堆内存设置:-Xms 与 -Xmx 设为相同值避免动态扩缩容带来的 GC 抖动;新生代大小建议为堆的 1/3~1/4
  • GC 选择:<4GB 且低延迟选 G1;大堆(>100GB)低延迟选 ZGC/Shenandoah;高吞吐批处理选 Parallel GC
  • GC 日志:JDK 9+ 统一使用 -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100m
  • OOM 自动 Dump:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path,结合 MAT/VisualVM 分析 Dominator Tree
  • 线上应急:jmap -dump 生成堆快照、jstack 打印线程栈(关注 BLOCKED 状态)、jstat -gcutil 观察 GC 趋势、arthas trace/monitor 定位热点方法
详细解答

G1 调优关键参数:-XX:MaxGCPauseMillis=200(目标停顿时间,默认 200ms),-XX:G1HeapRegionSize(Region 大小,根据堆自动计算,范围 1~32MB),-XX:InitiatingHeapOccupancyPercent=45(触发并发标记的老年代占比)。避免频繁 Full GC:检查是否存在大对象直接进入老年代、内存泄漏、或 Humongous 对象过多。

GC 日志分析要点:关注 GC 频率(Young GC 间隔 < 1s 可能新生代过小)、GC 耗时(单次 > 1s 需优化)、回收效率(每次 Young GC 回收量 / 新生代大小)。若老年代增长快且 Full GC 后回收少,可能存在内存泄漏。

MAT 分析 OOM:① 打开 Histogram 查看大对象;② Dominator Tree 找到保留堆最多的对象;③ Path to GC Roots 查看引用链;④ Leak Suspects 自动报告可疑泄漏点。常见泄漏:静态 Map 缓存未清理、ThreadLocal 未 remove、监听器未注销。

Arthas 线上诊断:thread -n 5 查看 CPU 最高的线程;trace com.example.Service getOrder '#cost>100' 追踪慢方法;dashboard 实时查看线程、内存、GC 概况;vmtool --action forceGc 手动触发 GC(慎用)。

工程实践

容器环境注意:JVM 默认按宿主机内存计算堆大小,需显式设置 -XX:+UseContainerSupport(JDK 8u191+ 默认开启)和 -XX:MaxRAMPercentage=75.0。K8s 中设置 resource limit 后 JVM 会自动识别,但最好显式配置避免 OOMKilled。

高频追问
  • Q: Full GC 频繁但堆内存充足是什么原因?A: 可能 System.gc() 被显式调用(如 NIO 的 DirectByteBuffer 分配)、元空间不足、大量反射类加载、或 JVM 参数显式禁用了并发回收。
  • Q: 如何在不重启的情况下获取堆 Dump?A: jmap -dump:live,format=b,file=heap.hprof <pid>;或 arthas heapdump。live 选项只 dump 存活对象,文件更小。
JVM GC OOM Arthas
E02.04
ZGC 染色指针与读屏障:染色位布局、并发标记转移重定位、多视图映射与分代 ZGC架构P1
核心要点
  • 染色指针(Colored Pointers):64 位指针的高 4 位用作 GC 标记位(Marked0/Marked1/Remapped/Finalizable),指针本身携带存活信息
  • 读屏障(Load Barrier):每次从堆加载对象引用时触发,检查染色位并按需"自愈"(重新标记/重映射),无需 STW 修正引用
  • 并发三阶段:标记/转移/重定位全并发,STW 仅在初始标记/再标记/初始转移几个极短节点(< 1ms)
  • 多视图映射(Multi-Mapping):同一物理内存映射到 M0/M1/Remapped 三个虚拟地址视图,染色位切换即"逻辑转移"
  • 分代 ZGC(JEP 439):年轻代/老年代独立染色指针集合,年轻代高频回收降低全堆扫描开销,吞吐提升 10%~20%
详细解答

染色位布局:x86-64 实际只用低 48 位虚拟地址(47 位用户态),高 16 位空闲;ZGC 借用高 4 位(bit 43/44/45/46)做染色,物理地址仍 52 位。4 位编码 16 种状态,ZGC 实际用 4 种核心状态:Marked0/Marked1(标记轮转)、Remapped(已重映射)、Finalizable(终结对象)。

并发标记:GC 线程与 Mutator 并发遍历对象图,通过染色位翻转(M0→M1)标识本轮标记;读屏障发现染色位过期则重新标记该引用,避免 STW 修正根集合。标记阶段无 STW。

转移与重定位:GC 选择回收价值高的 region 转移;转移后对象地址变化,染色位置为 Remapped;读屏障访问旧地址时触发 forwarding,自愈指针到新地址。重定位全并发,仅"初始转移集选定"需要极短 STW。

读屏障代价:每次引用加载多 1~3 条汇编指令(compare + branch + 可能的 fixup),吞吐量损失约 5%~10%;但换来暂停时间稳定 < 10ms,适合大堆低延迟场景。读屏障与 G1 的写屏障(CMS/G1 在引用写入时触发卡表更新)是两条技术路线。

STW 节点:初始标记(扫描根集合,< 1ms)、再标记(处理并发标记期间变更,< 1ms)、初始转移(选定转移集,< 1ms);其他全并发。

工程实践

启用参数:-XX:+UseZGC(JDK 15+ 生产可用),-XX:+ZGenerational 显式开启分代(JDK 21 需显式开启;JDK 23+ 已默认分代、无需该参数;JDK 24 该参数废弃,JDK 26 起移除,再写会导致 JVM 拒绝启动)。堆大小:ZGC 设计支持 16TB 堆,生产 32GB~512GB 表现最佳;小堆(< 4GB)优势不明显,G1 更合适。监控指标:GC 暂停时间(jcmd <pid> GC.Heap_info)、并发标记周期时长、染色指针翻转频率;目标暂停 < 10ms。吞吐量权衡:ZGC 吞吐量比 G1 低 5%~15%,但暂停稳定 < 10ms;延迟敏感(交易/风控/实时推荐)选 ZGC,吞吐优先(离线 ETL)选 G1。调优要点:避免大对象频繁分配(触发 ZGC 转移)、监控 ConcurrentHashMap 等引用密集结构的读屏障开销。

高频追问
  • Q: ZGC 为什么能保持 < 10ms 暂停?A: STW 阶段只做根集合扫描和转移集选定,对象图遍历/复制/重定位全并发;读屏障承担了"修正引用"的工作,无需 STW 修正整个对象图。
  • Q: 读屏障和写屏障有什么区别?A: 读屏障在引用加载时触发(ZGC/Shenandoah 用),写屏障在引用存储时触发(G1/CMS 用维护卡表/记忆集);ZGC 选读屏障是因为转移期间旧引用需即时自愈到新地址。
  • Q: 染色指针为什么不影响 64 位寻址?A: x86-64 实际只用低 48 位虚拟地址(47 位用户态),高 16 位空闲;ZGC 借用高 4 位做染色,物理地址仍 52 位,不影响真实寻址能力。
  • Q: 分代 ZGC 比单代快多少?A: 年轻代独立回收降低全堆标记频率,吞吐提升 10%~20%,分配停顿降低 50%+;适合分配速率高的对象密集型应用。JDK 21 引入但默认关闭,需显式开启;JDK 23 起默认分代。
ZGC 染色指针 读屏障 JVM

三、并发编程

本组知识链

本组从并发正确性出发:容器与 JMM → AQS/锁 → 线程池与 ThreadLocal → VT pinning 与 CAS 计数;始终先三性、后原语、再争用治理。

  1. 并发容器与 JMM:CHM 演进与 happens-before 决定「看起来对、并发下错」。
  2. AQS 与锁:排队、可重入、锁升级与虚拟线程适配有版本差异。
  3. 线程池与上下文:七参数与 Executors 陷阱;ThreadLocal 泄漏与 TTL 传递。
  4. VT 与 CAS:pinning 诊断按版本;LongAdder/VarHandle 解决热点计数与伪共享。
工程化要点
  • 锁顺序与死锁转储规范强制执行;多锁获取顺序可静态检查。
  • 无锁结构上线前做压力不变量测试,覆盖取消与中断路径。
  • VT 场景按 JDK 版本选择 pinning 诊断,并对照平台线程压测。
  • ThreadLocal 在池化场景必须有清理与泄漏检测,禁止串号。
  • 争用指标(持锁、CAS 失败、队列长度)默认接入看板。
  • 拒绝策略与饱和告警联动降级;无限队列禁止作为默认。
  • 热点计数优先评估分段累加,避免 AtomicLong 单点争用。
本组思维模式
  • 先三性:原子/可见/有序分清再选原语。
  • 成本结构:锁与 CAS 的代价要能量化到延迟预算。
  • 上下文显式化:隐式 ThreadLocal 是分布式链路的债。
  • 池化即风险面:拒绝、泄漏、串号与 VT pinning 同级管理。
  • 版本敏感:偏向锁废弃与 VT 适配按 JDK 基线验收。
E03.01
ConcurrentHashMap 1.7 vs 1.8:Segment 到 CAS+synchronized 的演进高级P0
核心要点
  • JDK 1.7:Segment 数组(默认 16)+ HashEntry 数组,每个 Segment 是独立可重入锁,锁粒度为段
  • JDK 1.8:取消 Segment,使用 Node 数组 + 链表/红黑树,锁粒度降为单个桶(synchronized 锁首节点)
  • 1.8 读操作基本无锁:使用 volatile + UNSAFE 保证可见性;写操作 CAS 失败后 synchronized 锁定首节点
  • 1.8 扩容支持多线程协助迁移(transfer 方法),每个线程负责一段 stride 的节点搬运
  • size 计算:1.7 直接加锁遍历;1.8 使用 CounterCell 数组分散热点,baseCount + sum 求和
详细解答

JDK 1.7 的 Segment 是粗粒度锁,虽然支持 16 线程并发写,但单 Segment 热点时仍会阻塞。且 Segment 数组不可扩容,一旦初始化固定大小。

JDK 1.8 的优化核心是将锁粒度从"段"降到"单个桶":

  • 如果桶为空:使用 CAS 直接插入新节点(tabAt + casTabAt)
  • 如果桶非空:synchronized 锁定链表/树的首节点,再进行插入/删除

读操作通过 volatile 保证可见性:Node 的 val 和 next 都是 volatile,数组用 UNSAFE.getObjectVolatile 读取。但读不保证一致性(弱一致性),可能读到旧值。

扩容时 1.8 支持多线程并发迁移。每个线程通过 transferIndex 领取一段 stride(默认 16),从后往前搬运节点。已完成迁移的位置标记为 ForwardingNode(hash = -1),其他线程遇到时知道该位置已处理。

工程实践

JDK 1.8 ConcurrentHashMap 的 size() 和 mappingCount() 是估计值,非精确值。若需精确计数,使用 LongAdder 或外部计数器。不要在循环中频繁调用 size() 做判断。

高频追问
  • Q: 为什么 1.8 用 synchronized 而不是 ReentrantLock?A: synchronized 在 JDK 1.6+ 引入锁升级(偏向锁→轻量级锁→重量级锁),且 JVM 对其有持续优化。锁单个 Node 时竞争概率低,synchronized 开销更小,代码也更简洁。
  • Q: ConcurrentHashMap 的弱一致性体现在哪?A: 读操作可能读到正在迁移的旧数据;迭代器创建后,其他线程的修改对迭代器不可见(fail-safe 而非 fail-fast)。
ConcurrentHashMap CAS 锁优化 并发容器
E03.02
JMM 内存模型:volatile/synchronized/final 的内存语义与 happens-before高级P0
核心要点
  • JMM 定义了线程与主内存的抽象关系:线程私有工作内存 + 共享主内存,所有变量存储在主内存
  • volatile:保证可见性(写立即刷回主内存,读从主内存刷新)和有序性(禁止指令重排序),但不保证原子性
  • synchronized:保证原子性、可见性、有序性;通过 Monitor 进入/退出实现,伴随内存屏障
  • final:正确发布后(this 引用不逃逸)保证不可变对象的可见性;JVM 在构造器末尾插入 StoreStore 屏障
  • happens-before 是 JMM 的核心规则,共 8 条:程序次序、监视器锁、volatile、线程启动/终止、中断、终结器、传递性
详细解答

volatile 的可见性通过内存屏障实现:写操作后插入 StoreLoad 屏障,将工作内存数据刷回主内存;读操作前插入 LoadLoad 屏障,从主内存重新读取。这保证了一个线程对 volatile 变量的修改对其他线程立即可见。

volatile 禁止指令重排序的规则:

  • 第二个操作是 volatile 写时,第一个操作不能重排序到其后
  • 第一个操作是 volatile 读时,第二个操作不能重排序到其前
  • 第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序

synchronized 的内存语义:线程释放锁时,会将工作内存中的共享变量刷回主内存(相当于 volatile 写);线程获取锁时,会将主内存中的共享变量刷新到工作内存(相当于 volatile 读)。

happens-before 规则中最容易被忽视的是传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。这是分析多线程代码执行顺序的关键工具。

工程实践

double-checked locking 必须用 volatile 修饰单例对象:private volatile static Singleton instance;,防止指令重排序导致其他线程读到未构造完成的对象。Java 5 之前的 JMM 有缺陷,即使 DCL 也不安全。

高频追问
  • Q: volatile 变量自增(i++)为什么不是线程安全的?A: i++ 是 read-modify-write 三个操作,volatile 只保证单个读/写的原子性,不保证复合操作的原子性。需使用 AtomicInteger 或 synchronized。
  • Q: final 字段在什么情况下可能看到未初始化的值?A: this 引用在构造器中逃逸(如启动线程、注册监听器),其他线程可能通过逃逸的引用读到 final 字段的默认值(0/null/false)。
扩展补全:CAS 与 ABA 问题
  • CAS 原理:compareAndSwap 是硬件级原子操作,原子地比较内存值与预期值,相等则更新为新值并返回成功;不相等则不更新并返回失败
  • ABA 问题:CAS 只比较"当前值",无法感知中间变化,需 AtomicStampedReference / AtomicMarkableReference 携带版本号或标记解决
  • CAS 性能陷阱:自旋重试在高竞争下 CPU 飙高,缓存行伪共享导致缓存行频繁失效

CAS 原理详解:CAS(Compare-And-Swap)是实现无锁并发的基础原语,三个操作数分别是内存位置 V、预期值 A、新值 B,只有当 V 的实际值等于 A 时才将 V 更新为 B,整个过程由 CPU 指令保证原子。硬件支撑方面,x86 平台依赖 cmpxchg 指令(配合 lock 前缀防止多核竞争),ARM 平台依赖 LDREX/STREX 独占访问指令对。Java 中的实现链路为 Unsafe.compareAndSwapXxx()(JDK 8 及以前)与 VarHandle.compareAndSet()(JDK 9+ 推荐的替代方案),java.util.concurrent 包的原子类(如 AtomicInteger)底层均基于此。CAS 失败后通常采用自旋重试(循环重试直到成功),自旋锁在低竞争场景开销极小,但高竞争场景下大量线程空转,CPU 占用飙升。

ABA 问题:场景为线程 1 读取值为 A,线程 2 将值从 A 改为 B 再改回 A,此时线程 1 执行 CAS 仍会成功,因为它只比较"当前值",无法感知中间被修改过。最典型的危害案例是无锁栈:线程 1 弹出栈顶节点(记为节点 X),线程 2 同时弹出并再次压入同一个节点 X,线程 1 的 CAS 虽然成功,但栈的实际结构已发生变化,可能引发悬垂引用或数据错乱。解决方案:① AtomicStampedReference:同时维护引用和版本号(stamp),每次修改版本号 +1,CAS 时同时比较引用与版本号;② AtomicMarkableReference:携带布尔标记(mark),适用于"是否被修改过"的二值场景(如判断数据是否过期)。

CAS 性能问题:① 自旋开销:高并发下大量 CAS 失败重试,CPU 飙高,可改用 LongAdder 替代 AtomicLong——内部为 base + Cell[] 分段结构,各线程先累加到自己的 Cell,降低竞争,读时再求和;② 缓存行伪共享:多个变量落在同一缓存行时,任一变量被修改都会导致整行失效,CAS 频繁触发缓存一致性协议(MESI)开销,可通过 @Contended 注解填充缓存行隔离。

JMM volatile happens-before 内存屏障
E03.03
Java 线程池:7 参数、Executors 缺陷、ForkJoinPool 工作窃取高级P0
核心要点
  • 7 参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler、unit
  • 执行流程:核心线程数 → 任务队列满 → 最大线程数 → 拒绝策略;非核心线程空闲超 keepAliveTime 后回收
  • Executors 缺陷:FixedThreadPool 和 SingleThreadPool 使用无界 LinkedBlockingQueue,可能 OOM;CachedThreadPool 允许无限创建线程;均不建议生产使用
  • ForkJoinPool:工作窃取(Work-Stealing)算法,每个线程有自己的双端队列,空闲时从其他线程队列尾部窃取任务
  • Java 21 Virtual Threads 下,使用 newVirtualThreadPerTaskExecutor(),由 JVM 调度轻量级线程,无需线程池管理
详细解答

线程池执行任务的完整流程:

  1. 当前运行线程数 < corePoolSize:创建新线程执行任务
  2. 当前运行线程数 ≥ corePoolSize:任务进入 workQueue
  3. workQueue 已满且运行线程数 < maximumPoolSize:创建非核心线程执行任务
  4. 运行线程数 ≥ maximumPoolSize 且队列满:执行拒绝策略

四种拒绝策略:

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:由调用线程(提交任务的线程)自己执行
  • DiscardPolicy:静默丢弃任务
  • DiscardOldestPolicy:丢弃队列最老的任务,然后重试提交

ForkJoinPool 的核心是工作窃取:每个 Worker 线程维护一个双端队列(Deque),新任务从队列头部 push,自己处理也从头部 pop。当线程空闲时,从其他随机选择的线程队列尾部 steal 任务。这种设计减少了线程间竞争(steal 操作在尾部,owner 操作在头部),适合递归分解型任务(如并行流、分治算法)。

Java 21 虚拟线程改变了线程池的使用范式:Executors.newVirtualThreadPerTaskExecutor() 为每个任务创建一个虚拟线程,由 JVM 自动调度到平台线程上。无需关心 corePoolSize、队列大小等参数,但需注意 pinning 问题。

工程实践

自定义线程池参数:corePoolSize = 最大并发数,maximumPoolSize = core × 1.5,队列用有界 ArrayBlockingQueue(容量 1000~5000),拒绝策略用 CallerRunsPolicy 做背压。线程名通过 ThreadFactory 自定义,方便排查。

高频追问
  • Q: 为什么核心线程数设置后不会自动回收?A: 默认 allowCoreThreadTimeOut=false,核心线程即使空闲也保留。设置 allowCoreThreadTimeOut=true 后,核心线程空闲超 keepAliveTime 也会回收。
  • Q: ForkJoinPool.commonPool() 的线程数是多少?A: 默认 Runtime.getRuntime().availableProcessors() - 1。可通过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism 调整。Parallel Stream 默认使用 common pool。
线程池 ForkJoin 拒绝策略 Virtual Threads
E03.04
Java AQS:CLH 变体队列、独占/共享模式、ReentrantLock 与 Semaphore 实现高级P1
核心要点
  • AQS(AbstractQueuedSynchronizer)是 JUC 锁/同步器的底层框架,基于一个 volatile int state 和一个 FIFO 双向队列(CLH 变体)实现
  • CLH 变体队列:每个线程封装为 Node,通过自旋 + CAS + LockSupport.park/unpark 实现阻塞和唤醒,相比原始 CLH 使用显式前驱指针,支持取消和超时
  • 独占模式:acquire/release,同一时刻只有一个线程能获取资源,如 ReentrantLock、ReentrantReadWriteLock.WriteLock
  • 共享模式:acquireShared/releaseShared,多个线程可同时获取资源,如 Semaphore、CountDownLatch、ReentrantReadWriteLock.ReadLock
  • ReentrantLock 公平锁:hasQueuedPredecessors() 检查队列是否有前驱节点,有则不去竞争,保证 FIFO;非公平锁允许插队,吞吐量更高
详细解答

AQS 的 acquire 流程:① tryAcquire(子类实现)尝试获取,成功返回;② 失败则 addWaiter(Node.EXCLUSIVE) 将当前线程加入队尾(CAS 保证原子性);③ acquireQueued 自旋/阻塞:若前驱是头节点则再次 tryAcquire,否则通过 shouldParkAfterFailedAcquire 将前驱状态改为 SIGNAL 后 park。

唤醒机制:release 时调用 unparkSuccessor,找到队列中第一个未取消的后继节点,调用 LockSupport.unpark。被唤醒线程从 park 处继续执行,再次尝试获取锁。这种设计避免了对所有等待线程的惊群效应。

ReentrantLock 的可重入:state 记录持有次数,同一线程多次 lock 则 state++,unlock 对应 state--,state=0 时真正释放。通过 ThreadLocal 或 AQS 的 exclusiveOwnerThread 记录持有线程。

条件队列(Condition):AQS 内部维护一个单向条件队列,await 时释放锁并加入条件队列,signal 时将节点从条件队列转移到同步队列尾部等待竞争。每个 ConditionObject 有独立的条件队列,而同步队列只有一个。

工程实践

ReentrantLock 相比 synchronized 的优势:支持公平/非公平、可中断 lock、超时获取、多条件变量。但 synchronized 在 JDK 6+ 经过偏向锁/轻量级锁/重量级锁优化后,单线程/低竞争场景性能优于 ReentrantLock。高竞争或需要高级特性时选 ReentrantLock。

高频追问
  • Q: 为什么 AQS 用 CLH 变体而非原始 CLH?A: 原始 CLH 是自旋锁,不适合长时间阻塞;AQS 变体结合自旋、CAS 和 park/unpark,支持超时、取消,且线程状态由前驱节点的 waitStatus 控制。
  • Q: Semaphore 的公平与非公平区别?A: 公平 Semaphore 按 FIFO 分配许可;非公平允许新线程插队获取,吞吐量更高但可能导致饥饿。
AQS ReentrantLock 并发 JUC
E03.05
ThreadLocal:ThreadLocalMap、弱引用、内存泄漏、InheritableThreadLocal 与 TransmittableThreadLocal高级P1
核心要点
  • 实现原理:每个 Thread 对象内部有一个 ThreadLocalMap(ThreadLocal.ThreadLocalMap),key 为 ThreadLocal 实例(弱引用),value 为业务对象(强引用)
  • 弱引用设计:ThreadLocal 作为 key 使用 WeakReference,当 ThreadLocal 外部强引用消失后,key 可被 GC 回收,但 value 仍存在,导致内存泄漏
  • 内存泄漏条件:线程池场景下线程长期存活,ThreadLocal 未调用 remove(),且 ThreadLocal 外部引用已断 → key 为 null,value 无法被访问也无法被回收
  • 防护手段:① finally 中调用 remove();② 使用 static 修饰 ThreadLocal 保证 key 强引用存活;③ 使用 TransmittableThreadLocal(TTL)自动清理和传递
  • InheritableThreadLocal:子线程继承父线程的 ThreadLocal 值,但线程池场景下线程复用会导致值错乱,需使用 TTL 解决
详细解答

ThreadLocalMap 结构:开放寻址法解决哈希冲突的 Entry 数组,下标计算为 threadLocal.threadLocalHashCode & (len - 1)。发生哈希冲突时向后线性探测。扩容阈值为数组长度的 2/3。

内存泄漏根因:线程池中的线程是复用的,线程的 ThreadLocalMap 生命周期与线程相同。若某次任务设置了 ThreadLocal 值但未 remove,下次任务复用该线程时旧值仍存在。即使 ThreadLocal 被回收(key=null),Entry 的 value 仍被当前线程强引用,无法回收。

探测清理:get/set/remove 时会触发 expungeStaleEntry,扫描并清理 key=null 的 Entry。但这只是补救,无法替代主动 remove。

TransmittableThreadLocal(Alibaba TTL):通过装饰线程池(TtlExecutors)和捕获/回放机制,在任务提交时捕获父线程 TTL 值,执行前设置到当前线程,执行后清理。解决了线程池复用和父子线程传递两个问题。

工程实践

务必在 finally 块中调用 remove()。Spring 的 RequestContextHolder 使用 ThreadLocal 存储请求上下文,框架内部已处理清理。日志上下文(MDC)在线程池场景下必须配合 TTL 使用,否则 Trace ID 会错乱。

高频追问
  • Q: 为什么 ThreadLocalMap 不用链表解决冲突?A: ThreadLocalMap 是为线程私有的轻量结构,设计时考虑空间效率和简洁性。开放寻址法在装载因子低时缓存友好,且 ThreadLocal 数量通常不多。
  • Q: 一个线程可以有多个 ThreadLocal 吗?A: 可以。ThreadLocalMap 的 key 就是 ThreadLocal 实例本身,每个 ThreadLocal 对应一个 Entry。
ThreadLocal 内存泄漏 线程池 TTL
E03.06
Virtual Threads pinning 排查:载体线程钉住诊断、synchronized 与 native 钉住机制、JFR 监控与治理策略架构P1
核心要点
  • Pinning 定义:虚拟线程在 synchronized 块或 native 方法中无法 unmount 载体线程,导致 ForkJoinPool carrier 被阻塞、吞吐量坍塌
  • 两大根因:synchronized monitor(JDK 21 限制,JEP 491 在 JDK 24 修复)+ native 栈帧(JNI/第三方 native 库,Continuation 无法拷贝/恢复 native 帧)
  • 排查工具(按版本):JDK 21–23 可用 -Djdk.tracePinnedThreads=full/short;JDK 24 起该属性移除,改用 JFR jdk.VirtualThreadPinned(默认阈值 20ms)与新式线程转储;可辅以 async-profiler
  • 治理手段:JDK 21–23 热点 synchronized → ReentrantLock;JDK 24+(JEP 491)不必仅为 pinning 回避 synchronized,改锁按功能需求;native 调用隔离到独立线程池
  • 监控指标:载体线程阻塞时长、虚拟线程 mount/unmount 次数、Pinning 事件计数与堆栈 TOP-N
详细解答

调度模型:虚拟线程由 ForkJoinPool(默认 CPU 核数个 carrier)调度,mount 到 carrier 上执行;遇阻塞 IO 调用 Continuation.yield 主动 unmount,carrier 继续跑其他虚拟线程。Pinning 打破了这一闭环。

synchronized 钉住:monitorenter/monitorexit 依赖对象 ObjectMonitor,JDK 21 在 monitor 持有期间禁止 unmount(Continuation 与 ObjectMonitor 协作未完成),载体线程被绑定。JDK 24 JEP 491 改造为类似 Object.wait 的可卸载机制,synchronized 不再 pin。

native 钉住:native 栈帧无法被 Continuation 拷贝/恢复,进入 JNI 调用即钉住,JDK 24 仍未解决。重型 native 库(如压缩/加密/旧版 JNI SDK)是高危点。

工程影响:HikariCP(5.1 前用 synchronized)、旧版 Netty、部分数据库驱动(synchronized Socket)、第三方 SDK 是 pinning 高发区;线上表现是"虚拟线程开了但 QPS 没涨、carrier 线程 CPU 100%"。

工程实践

JDK 21–23:可用 -Djdk.tracePinnedThreads=full|short 打印钉住栈(生产可 short 降噪);热点 synchronized 可改 ReentrantLock。JDK 24+:该系统属性已移除,持续监控改用 JFR jdk.VirtualThreadPinned(阈值可从默认 20ms 调到 10ms),现场快照用新式 Thread.dump_to_file;JEP 491 后不必仅为 pinning 回避 synchronized,native/JNI 仍须隔离到独立线程池。HikariCP 5.1+ 已适配虚拟线程。版本对照真源:C04.09。

高频追问
  • Q: 虚拟线程一定能提升吞吐量吗?A: 仅对 IO 密集型有效;CPU 密集型反而因 Continuation 调度开销劣化;阻塞点全在 synchronized/native 上时等于退化成平台线程,QPS 不会涨。
  • Q: synchronized 换 ReentrantLock 有什么代价?A: 内存略高、需手动 unlock、公平锁更慢。JDK 21–23 为消 pinning 常值得换;JDK 24+(JEP 491)改锁应出于公平锁/可中断/超时/Condition 等功能需求,而非“凡 synchronized 必换”。
  • Q: Pinning 与死锁如何区分?A: jstack 看 carrier 线程状态:RUNNABLE 但长时间无进展是 pinning(被 native/synchronized 钉住但 OS 仍可调度),BLOCKED 是锁竞争/死锁。Pinning 不会永久阻塞,但有性能损失。
  • Q: 为什么 JDK 21 不直接修复 synchronized pinning?A: 涉及 JVM ObjectMonitor 与 Continuation 协作的深度改造,工程量大;JEP 491 历经多个 incubator/preview 才在 JDK 24 稳定,21 LTS 期只能规避。
虚拟线程 JDK 21 性能排查 并发
E03.07
synchronized 锁升级:偏向锁废弃、轻量级锁与重量级锁、Mark Word 布局、JEP 491 虚拟线程适配高级P1
核心要点
  • 锁升级路径:无锁 → 偏向锁(已废弃)→ 轻量级锁(自旋 CAS)→ 重量级锁(OS Mutex Park)
  • Mark Word(64 位)按锁状态复用同一组比特,不是 HashCode 与 ThreadID 同时常驻:无锁态可存 identity hash;偏向锁时代偏向态写入 ThreadID(与 hash 互斥——已算 hash 则无法偏向);轻量级锁对象头指向栈上 Lock Record;重量级锁指向 ObjectMonitor。偏向锁废弃后状态简化为无锁 / 轻量级 / 重量级,原偏向相关位回收
  • JDK 15 废弃并默认关闭偏向锁(JEP 374),BiasedLockingStartupDelay 默认值从 4000ms 改为 0ms;JDK 18 起移除该启动参数,Mark Word 中偏向锁位被回收
  • 轻量级锁:线程栈帧 Lock Record 拷贝 Mark Word,CAS 替换对象头指向 Lock Record;竞争时自旋重试,避免 OS 互斥量开销
  • 重量级锁:自旋失败后膨胀为 ObjectMonitor,线程 park 进入等待队列;JDK 24+(JEP 491)重构 ObjectMonitor 为可虚拟化,synchronized 块内 park 时虚拟线程可正常卸载
详细解答

Mark Word 状态复用:对象头只有一份 Mark Word,各锁态覆盖解释同一比特区(类似 union),切勿把教材里「HashCode 位 + ThreadID 位」读成同时占满。工程上用 JOL 观察时,同一对象在 hashCode() 前后、加锁前后的头布局会变化;JDK 15+ 默认关闭偏向锁后,日常更常见的是无锁 hash 布局与轻量级/重量级指针布局。

锁升级流程:① 对象初始为无锁状态(标志位 01);② 偏向锁——首个线程进入同步块时,CAS 将 ThreadID 写入 Mark Word,后续同线程重入只需比对 ThreadID(无 CAS 无自旋);③ 轻量级锁——出现第二个线程竞争时,偏向锁撤销,线程栈帧 Lock Record 拷贝当前 Mark Word,CAS 将对象头指向 Lock Record 指针(标志位 00),竞争方自旋重试;④ 重量级锁——自旋超过阈值(默认自适应)或等待线程过多,膨胀为 ObjectMonitor(标志位 10),竞争失败线程 park 进入 EntryList。

偏向锁废弃背景(JEP 374,JDK 15):偏向锁的收益依赖"同一线程重入"场景,但现代应用大量使用线程池轮转执行 + synchronized 粒度已较细,偏向锁撤销(safepoint 全局 STW)的开销反而超过收益。HotSpot 维护偏向锁代码增加了 synchronized 与虚拟线程协作改造的复杂度,废弃后锁状态从 4 种简化为 3 种(无锁/轻量级/重量级),代码更易维护。

JEP 491 与虚拟线程:JDK 21 虚拟线程在 synchronized 块内 park 会被"钉住"在载体线程上(ObjectMonitor 与 Continuation 协作未完成),载体线程被阻塞导致吞吐坍塌。JDK 24+ 重构 ObjectMonitor 实现,使其可虚拟化——synchronized 块内 park 时虚拟线程可正常 unmount 载体线程,消除了最主要的 pinning 源。JDK 24+ 不必再为 pinning 回避 synchronized。

紧凑对象头(JEP 519,JDK 25+):可选启用 -XX:+UseCompactObjectHeaders(默认关闭),将对象头压缩至 64 位(传统默认 96 位),Mark Word 位布局有调整。启用后 Mark Word 仍需容纳 HashCode + GC 年龄 + 锁标志位,轻量级锁的 Lock Record 机制不变,但可用位数减少。

工程实践

JDK 21 虚拟线程场景:若 synchronized 密集且 IO 阻塞多,优先升级 JDK 24+(JEP 491 根治 pinning)或改用 ReentrantLock(await/signal 可让虚拟线程 unmount)。JDK 15+ 不再依赖偏向锁,低竞争场景 synchronized 性能已足够好,无需刻意改用 ReentrantLock。使用 JOL(Java Object Layout)观察对象头布局时需注意是否启用了紧凑对象头。

高频追问
  • Q: 偏向锁为什么在 JDK 15 废弃而不是直接删除?A: 废弃(Deprecated)给了生态适配窗口——先默认关闭(BiasedLockingStartupDelay=0),JDK 18 才移除启动参数,完全清理代码在后续版本渐进完成。避免一次性删除导致依赖偏向锁行为的旧应用性能回归无法排查。
  • Q: 轻量级锁自旋的退出条件是什么?A: 自旋采用自适应策略(Adaptive Spinning):① 固定次数限制(避免 CPU 空转);② 历史成功率动态调整——上次自旋成功则增加自旋次数,上次失败则减少甚至跳过自旋直接膨胀。自适应策略使 JVM 根据运行时竞争强度自动调优。
  • Q: JDK 24+ synchronized 不再 pin 虚拟线程,ReentrantLock 还有必要吗?A: 仍有必要——ReentrantLock 支持公平锁、可中断 lock、超时 tryLock、多条件变量(Condition)、非块状锁定(跨方法 lock/unlock)。synchronized 只解决 pinning,不解决功能差异。需要这些高级特性时仍用 ReentrantLock。
  • Q: 重量级锁的 ObjectMonitor 与 AQS 有何本质区别?A: ObjectMonitor 是 JVM 内置的 Monitor 实现(C++),通过 OS Mutex/Park 完成;AQS 是 Java 层框架,通过 CLH 变体队列 + LockSupport.park/unpark 完成。ObjectMonitor 的 EntryList 是平台相关的;AQS 的等待队列可被 Java 代码精细控制(取消、超时、条件变量)。
synchronized 锁升级 Mark Word JEP 491
E03.08
CAS 原子类与 LongAdder:AtomicInteger/AtomicLong 瓶颈、LongAdder 分段累加、VarHandle 替代 Unsafe、StampedLock 乐观读高级P1
核心要点
  • CAS 原理:CPU 指令级原子操作(x86 lock cmpxchg,ARM LDREX/STREX),Java 通过 Unsafe.compareAndSwapXxx(JDK 8)或 VarHandle.compareAndSet(JDK 9+)暴露
  • 原子类层级:AtomicInteger/AtomicLong(单变量 CAS)→ AtomicReference(引用 CAS)→ AtomicStampedReference(版本号防 ABA)→ LongAdder/DoubleAdder(分段累加降低竞争)
  • LongAdder 原理:base + Cell[] 分段,各线程先累加到自己的 Cell(CAS 失败则换 Cell),读时 sum(base + allCells);高并发下竞争分散到多个 Cell,比 AtomicLong 的单点 CAS 快数倍
  • VarHandle(JDK 9+):替代 Unsafe 的官方 API,提供类型安全的 CAS/read/write 操作与内存排序控制(MemoryMode),Unsafe 在后续版本逐步移除
  • StampedLock(JDK 8+):乐观读锁 + 悲观读锁 + 写锁三模式,乐观读无锁(stamp 校验),适合读多写少且能容忍乐观读失败重试的场景
详细解答

AtomicLong 高并发瓶颈:所有线程 CAS 竞争同一个 value 字段,高并发下大量 CAS 失败重试 + 缓存行频繁失效(MESI 协议),CPU 飙高但有效吞吐不增。LongAdder 将竞争分散——内部维护 base + Cell[] 数组(默认 CPU 核数个 Cell),写线程通过 ThreadLocalRandom 探测哈希分配到不同 Cell,仅当所有 Cell 都 CAS 失败时 fallback 到 base。读时 sum() 遍历所有 Cell 求和(非精确一致,最终一致)。

LongAdder vs AtomicLong 选型:① 计数统计场景(QPS/访问量/延迟采集)选 LongAdder——高并发写、低精度读即可;② 精确值场景(序列号/余额)选 AtomicLong——需强一致读;③ 低并发两者差异可忽略,AtomicLong 更简单。LongAdder 内存占用更高(Cell[] 数组),sum() 不保证原子一致性。

VarHandle 替代 Unsafe:JDK 9 引入 java.lang.invoke.VarHandle,通过 MethodHandles.lookup().findVarHandle(...) 获取字段句柄,提供 compareAndSet/getAndAdd/getOpaque/getAcquire 等操作,显式指定内存可见性语义。sun.misc.Unsafe 是内部 API,JDK 逐步移除(JEP 471 在 JDK 23+ 移除部分功能),新代码应使用 VarHandle。

StampedLock 乐观读:① tryOptimisticRead() 返回 stamp(无锁);② 读完后 validate(stamp) 校验期间是否有写操作(通过 volatile stamp 字段比对);③ 校验失败则升级为悲观读锁重试。优势:读路径完全无 CAS 无自旋,在读远多于写时性能优于 ReentrantReadWriteLock。注意:StampedLock 不可重入,悲观写锁会阻塞所有乐观读(validate 返回 false)。

工程实践

高并发计数器:LongAdder 替代 AtomicLong,8 核以上高竞争场景吞吐提升 2-5 倍。读写锁选型:读多写少且读操作短 → StampedLock(乐观读零开销);需可重入或 Condition → ReentrantReadWriteLock。缓存行伪共享:@Contended 注解 + -XX:-RestrictContended 启用,对高频 CAS 字段填充隔离缓存行。

高频追问
  • Q: LongAdder 的 sum() 为什么不保证一致性?A: sum() 遍历 Cell[] 时其他线程可能正在更新某个 Cell,读到的是旧值;但最终(所有写线程停止后)sum 一定收敛到正确值。这是"最终一致"语义,适合统计场景,不适合精确控制流(如 compareAndSet 决策)。
  • Q: VarHandle 相比 Unsafe 有什么安全优势?A: ① 类型安全——VarHandle 绑定字段类型,编译期检查;② 内存排序显式——通过 MemoryMode 指定 ACQUIRE/RELEASE/OPAQUE 等语义,而非 Unsafe 的全屏障;③ 官方 API 稳定——Unsafe 是 sun.misc 内部 API,JEP 471 在 JDK 23+ 逐步移除,VarHandle 是 long-term 替代。
  • Q: StampedLock 的乐观读失败率高时性能如何?A: 写操作频繁时 validate 失败率高,频繁升级悲观读锁重试,性能退化为甚至不如 ReentrantReadWriteLock(多了 stamp 校验开销)。StampedLock 适合写操作占比 <5% 且读操作较短的场景。
  • Q: ABA 问题在什么场景下会引发实际 Bug?A: 无锁栈/队列——节点被弹出后重新压入,CAS 误判"未变化";资源池——对象被归还后被取出再归还,状态检查 CAS 误判"未被使用"。金额/计数场景一般不受 ABA 影响(值本身就是状态)。AtomicStampedReference 携带版本号解决。
CAS LongAdder VarHandle StampedLock
E03.09
Java 并发容器全景:ConcurrentHashMap、COW、阻塞队列族、无锁队列与计数器的选型高级P1
核心要点
  • 键值:ConcurrentHashMap(JDK8 Node+CAS+synchronized,size 用 baseCount+CounterCell,transfer 并发扩容)
  • 读多写少:CopyOnWriteArrayList / CopyOnWriteArraySet(写时复制,读无锁弱一致)
  • 队列族:ArrayBlockingQueue / LinkedBlockingQueue / PriorityBlockingQueue / DelayQueue / SynchronousQueue / LinkedTransferQueue / ConcurrentLinkedQueue(无锁 CAS)
  • 有序并发:ConcurrentSkipListMap / ConcurrentSkipListSet(跳表,范围查询 O(log n))
  • 热点计数:LongAdder / DoubleAdder(分段 Cell,Striped64)
  • 屏障与分治:Phaser / Exchanger / CyclicBarrier、ForkJoinPool(工作窃取)
详细解答

ConcurrentHashMap 的并发语义(见 E03.01)是容器族的基准:读基本无锁、写 CAS+synchronized 首节点、size() 弱一致。在此之上,CopyOnWrite 适合监听器/路由表等读远多于写的场景,代价是写时复制整份数组、内存翻倍且读可能看到旧快照;不要用它承载频繁写入的大列表。

BlockingQueue 是生产者-消费者与线程池的骨架。选型:有界且公平用 ArrayBlockingQueue(单锁、固定容量);高吞吐可选 LinkedBlockingQueue(双锁、可选有界);定时/延时任务用 DelayQueue;线程池拒绝策略或「直接交接」用 SynchronousQueue(容量 0,握手式传递);需要 transfer/多角色交接用 LinkedTransferQueue。ConcurrentLinkedQueue 用 Michael-Scott CAS 算法无锁入队出队,高并发吞吐优于锁队列,但 size() 弱一致、禁止 null。

ConcurrentSkipListMap 提供线程安全的有序 Map(替代 Collections.synchronizedSortedMap),适合需要范围查询/有序遍历的并发场景。LongAdder 在热点计数上显著优于 AtomicLong:把单点竞争分散到多个 Cell,sum() 时累加;代价是 sum() 弱一致、读取有偏差,不适合需要精确即时值的控制流。ForkJoinPool 用工作窃取双端队列做分治,并行流与 CompletableFuture 默认共用 CommonPool;陷阱是 IO/阻塞任务别丢进 ForkJoinPool(会饿死其他任务),且要控制递归拆分粒度。

工程实践

选型口诀:键值并发用 ConcurrentHashMap;读多写少用 COW;解耦生产与消费用 BlockingQueue(容量与拒绝策略先定);有序并发用 SkipListMap;热点计数用 LongAdder;分治并行用 ForkJoinPool。统一警惕「弱一致性」——size()/isEmpty()/迭代器只反映某一时刻快照,不能用于精确控制流。

高频追问
  • Q: ConcurrentHashMap.computeIfAbsent 有坑吗?A: JDK8 在值计算过程中对同一桶再 put 会触发递归锁(死锁风险),JDK9+ 已修复为只锁首节点且禁止重入计算;生产仍建议避免在 compute 回调里做重操作或再写同表。
  • Q: ThreadLocal 算并发容器吗?为什么线程池下会泄漏?A: ThreadLocal 是线程私有存储,不算共享容器。线程池复用线程时 key 为弱引用、value 为强引用且不会被 GC,若不清 remove() 会累积泄漏;务必用 try-finally 收尾清理。
并发容器 ConcurrentHashMap BlockingQueue LongAdder ForkJoinPool

四、Spring 生态

本组知识链

本组沿 Spring 运行时到 AI 扩展:循环依赖与自动配置 → 事务/AOP/MyBatis → Spring AI RAG → Agent/Tool/MCP 护栏。

  1. 容器机制:三级缓存循环依赖边界;自动配置 Conditional 链决定装配成败。
  2. 事务与 AOP:传播/隔离/失效场景;JDK 代理 vs CGLIB 与自调用陷阱。
  3. 数据访问:MyBatis 缓存与插件拦截;N+1 与映射边界。
  4. AI 扩展:RAG 分块检索重排;Agent 工具合约、步数熔断;MCP 与 Function Calling 分工。
工程化要点
  • 循环依赖只允许在评审过的边界内;新增依赖优先重构而非开开关。
  • 事务失效清单(自调用、checked 异常、多线程)进 Code Review 清单。
  • AOP 代理类型与 final/私有方法限制写入团队规范。
  • MyBatis 二级缓存默认关闭,开启必须论证一致性窗口。
  • RAG 上线绑定黄金集与检索评测,禁止只看 demo 问答。
  • Agent 工具调用必须幂等分级与步数/金额熔断。
  • MCP/工具供给权限边界与审计日志进发布门禁。
本组思维模式
  • 装配可解释:自动配置失败先画 Conditional 链。
  • 代理边界:事务与 AOP 问题先问是不是同一 Bean 调用。
  • 一致性窗口:缓存与事务隔离级别写进契约。
  • AI 同发版:提示词/索引/工具契约与代码同级变更。
  • 护栏优先:能力开放前先定副作用与熔断。
E04.01
Spring 循环依赖:三级缓存源码级解析与局限性架构P1
核心要点
  • 三级缓存:singletonObjects(成品池)、earlySingletonObjects(半成品池)、singletonFactories(工厂池)
  • 循环依赖只能解决单例 Bean 的 setter/field 注入;构造器注入无法解决(Bean 未实例化完)
  • 关键机制:A 实例化后暴露 ObjectFactory 到三级缓存,B 注入 A 时从工厂获取早期引用
  • 如果 Bean 需要代理,ObjectFactory 会提前生成代理对象,否则返回原始对象
  • Spring Boot 2.6+ 默认禁止循环依赖,需显式开启 spring.main.allow-circular-references=true
详细解答

Spring 解决循环依赖的核心在于"提前暴露"。当 A 开始实例化(createBeanInstance)后,尚未填充属性(populateBean)时,就将一个 ObjectFactory 放入三级缓存(singletonFactories):

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

如果 B 依赖 A,在填充 B 的属性时会去容器找 A。此时 A 尚未完成,但三级缓存中有 A 的工厂,调用 getObject() 获取早期引用。如果 A 需要被代理(如 AOP),getEarlyBeanReference 会调用 SmartInstantiationAwareBeanPostProcessor 提前生成代理对象,否则返回原始对象。

获取早期引用后,A 的工厂从三级缓存移除,早期引用放入二级缓存(earlySingletonObjects)。当 A 完成属性填充和初始化后,从二级缓存移到一级缓存(singletonObjects)。

为什么不用二级缓存?因为二级缓存只存对象引用,无法处理"是否需要代理"的判断。三级缓存的 ObjectFactory 可以延迟决定返回原始对象还是代理对象。

构造器注入无法解决:因为构造器注入发生在实例化阶段,此时 Bean 尚未创建完成,无法暴露早期引用。

工程实践

推荐通过重构消除循环依赖(如引入中间层、事件驱动)。若必须使用,优先用 setter/field 注入,避免构造器注入。开启 @Lazy 延迟加载也是一种方案。

高频追问
  • Q: 为什么 Spring 不用二级缓存,一定要三级?A: 二级缓存只能存固定对象,无法处理 AOP 代理的延迟创建。三级缓存的 ObjectFactory 允许在需要时才决定是否生成代理,保持灵活性。
  • Q: @Async 代理导致的循环依赖问题?A: @Async 由 AsyncAnnotationBeanPostProcessor 代理,若 A 和 B 相互依赖且都有 @Async,可能因代理时机问题导致注入的是原始对象而非代理对象,引发异步失效。
Spring 循环依赖 三级缓存 AOP 代理
E04.02
Spring Boot 自动配置:@EnableAutoConfiguration + AutoConfiguration.imports + @Conditional高级P1
核心要点
  • @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 加载 META-INF/spring.factories 中 EnableAutoConfiguration 键对应的配置类列表
  • Spring Boot 2.7+ 引入 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 新配置方式,spring.factories 中自动配置类条目自 2.7 起标记废弃,Spring Boot 3.0 起完全移除,新项目应统一使用 AutoConfiguration.imports
  • @Conditional 家族控制条件装配:@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等
  • 自动配置类通常标注 @AutoConfiguration(替代 @Configuration),并指定 after/before 顺序
  • spring-boot-autoconfigure 模块包含所有官方 Starter 的自动配置类
详细解答

自动配置的入口是 @SpringBootApplication,它组合了 @EnableAutoConfiguration。AutoConfigurationImportSelector 实现 DeferredImportSelector,在所有用户配置类处理完毕后再执行,确保用户 Bean 优先。

加载流程:

  1. 从所有依赖jar包的 META-INF/spring.factories 读取 EnableAutoConfiguration 全类名列表(Spring Boot 3.0 起改为读取 AutoConfiguration.imports 文件)
  2. 使用 SpringFactoriesLoader 加载,去重后得到候选配置类
  3. 排除 @EnableAutoConfiguration 的 exclude/excludeName 指定的类
  4. 通过 @Conditional 条件过滤,只有条件匹配的类才会被注册

常用 @Conditional 注解:

  • @ConditionalOnClass:类路径存在指定类时生效(如 classpath 有 RedisOperations 才配 RedisTemplate)
  • @ConditionalOnMissingBean:容器中不存在指定 Bean 时生效(允许用户自定义覆盖)
  • @ConditionalOnProperty:配置文件中指定属性匹配时生效
  • @ConditionalOnWebApplication:当前是 Web 应用时生效

Spring Boot 3.0 后,自动配置类需使用 @AutoConfiguration 替代 @Configuration,以明确标识其为自动配置入口。

工程实践

自定义 Starter 时,在 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中写入自动配置类全名。使用 @ConditionalOnMissingBean 保留用户覆盖能力。通过 @AutoConfigureAfter 控制依赖其他自动配置的顺序。 测试策略(Testcontainers / 契约测试 / ArchUnit)见篇章 C06.14。

高频追问
  • Q: 为什么自动配置要用 DeferredImportSelector?A: 延迟到所有普通 @Configuration 处理完后再执行,确保用户自定义的 Bean 定义优先于自动配置,实现"约定优于配置,配置覆盖约定"。
  • Q: 自动配置类中的 @Bean 方法为什么通常是 @ConditionalOnMissingBean?A: 给用户保留自定义覆盖的能力。如果用户自己定义了同类型 Bean,自动配置不再注入,避免冲突。
Spring Boot 自动配置 SPI Starter
E04.03
Spring AI RAG:文档分块、Embedding、向量检索、重排序、落地边界架构P0
核心要点
  • RAG(Retrieval-Augmented Generation):检索增强生成,先检索外部知识,再让 LLM 基于检索结果生成回答,解决大模型幻觉和知识时效问题
  • 文档分块:按固定大小(如 512 token)、段落、语义或递归字符分割,块大小影响检索精度和上下文长度
  • Embedding:通过向量化模型(如 text-embedding-3、BGE)将文本转为高维向量(通常 768/1024/1536 维)
  • 向量检索:HNSW(近似最近邻)或 IVF 索引加速,余弦相似度/点积计算语义相关性
  • 重排序(Rerank):先用向量检索召回 Top-K,再用交叉编码器(Cross-Encoder)精排,提升相关性
详细解答

RAG 的典型流程:文档加载 → 分块 → Embedding → 存入向量数据库 → 用户查询时先 Embedding → 向量相似度检索 Top-K 块 → 将检索结果作为上下文送入 LLM Prompt → LLM 生成回答。

分块策略对比:

  • 固定字符分块:简单高效,但可能在句子中间截断,丢失语义
  • 递归字符分块:优先按段落、句子、单词层级分割,尽量保持语义完整
  • 语义分块:通过 Embedding 相似度判断语义边界,动态决定切分点
  • 重叠分块(Overlap):相邻块共享部分内容,避免边界信息丢失

Spring AI 提供统一的 RAG 抽象:DocumentReader 读取文档,TokenTextSplitter 分块,EmbeddingModel 生成向量,VectorStore(如 PgVector、Redis、Milvus)存储和检索。RAG 链通过 Advisor 机制在 ChatClient 调用前自动注入检索上下文。

重排序的必要性:向量检索(Bi-Encoder)是独立编码查询和文档,无法捕捉查询与文档的细粒度交互。Cross-Encoder 将查询+文档拼接后输入模型,通过注意力机制计算精确相关性得分,效果更好但速度较慢。生产环境通常采用"向量检索召回 100 条 + Cross-Encoder 重排取 Top-5"的两阶段策略。

工程实践

向量数据库选型:小规模用 PgVector(PostgreSQL 插件),中等规模用 Redis Stack 或 Milvus,大规模用 Pinecone/Weaviate。分块大小建议 256~512 token,重叠 20%。RAG 评估指标:上下文召回率(Context Recall)、答案相关性(Answer Relevance)、忠实度(Faithfulness)。

高频追问
  • Q: RAG 和 Fine-Tuning 的区别和选型?A: RAG 适合频繁变化的知识(如文档、FAQ),无需训练,成本低;Fine-Tuning 适合固定领域知识且需改变模型行为风格,需要标注数据和训练成本。两者可结合:RAG 提供上下文,Fine-Tuned 模型做生成。
  • Q: 向量检索的局限性和优化方向?A: 向量检索基于语义相似度,对关键词精确匹配(如产品型号、身份证号)效果差。优化:混合检索(向量 + 全文搜索 BM25),或 Metadata Filtering 先过滤再向量检索。
Spring AI RAG 向量检索 Embedding
E04.04
Spring 事务:传播机制、隔离级别、失效场景、@Transactional 底层与编程式事务高级P0
核心要点
  • 传播机制(Propagation):REQUIRED(默认,加入当前事务)、REQUIRES_NEW(挂起当前事务,新建独立事务)、NESTED(嵌套事务,基于 Savepoint)、SUPPORTS/MANDATORY/NOT_SUPPORTED/NEVER
  • 隔离级别:DEFAULT(随数据库,通常 RC)、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE
  • @Transactional 失效场景:非 public 方法、同类内部调用(this 代理绕过)、异常被捕获未抛出、rollbackFor 配置不当(默认只回滚 RuntimeException/Error)、异步方法
  • 底层实现:Spring 通过 AOP 代理(JDK 动态代理/CGLIB)拦截目标方法,事务前后通过 PlatformTransactionManager 开启/提交/回滚
  • 编程式事务:TransactionTemplate 或 PlatformTransactionManager 手动控制,适合复杂事务逻辑(如部分回滚、条件提交)
详细解答

同类内部调用失效原因:Spring AOP 代理只能拦截外部方法调用,this.method() 是目标对象直接调用,不经过代理。解决:① 注入自身代理(@Autowired self);② 使用 AopContext.currentProxy();③ 拆分到另一个 Bean。

REQUIRES_NEW 与 NESTED 的区别:REQUIRES_NEW 是完全独立的新事务,回滚不影响外层;NESTED 是外层事务的 Savepoint,回滚只回滚到 Savepoint,外层事务仍可提交。NESTED 依赖数据库 Savepoint 支持(如 JDBC)。

事务超时:@Transactional(timeout=5) 表示事务必须在 5 秒内完成,从事务开始计时。仅对需要新事务的传播行为有效(REQUIRED 在加入已有事务时不生效,REQUIRES_NEW 生效)。

只读事务:@Transactional(readOnly=true) 是优化提示,Spring 会设置 JDBC Connection 的 readOnly 标志,Hibernate 会放弃脏检查,部分数据库可利用只读副本。但不保证强制只读。

工程实践

事务方法应精简,避免长事务(RPC 调用、IO 操作不要放在事务内)。大事务会增加锁持有时间,降低并发。对于分布式事务,Spring 本地事务无法保证,需引入 Seata 或 Saga。建议显式配置 rollbackFor = Exception.class,避免遗漏受检异常。

高频追问
  • Q: 同一个类中 A 方法(无事务)调用 B 方法(@Transactional),B 事务生效吗?A: 不生效。内部调用绕过代理。若 A 也有事务且为 REQUIRED,B 会加入 A 的事务。
  • Q: 事务方法抛出异常被 catch 后如何回滚?A: catch 后需再次抛出异常,或调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记回滚。
Spring 事务 AOP 传播机制
E04.05
MyBatis:一级/二级缓存、插件拦截原理、SQL 执行流程、懒加载与结果映射高级P1
核心要点
  • 一级缓存(Local Cache):SqlSession 级别,默认开启,同一会话内相同 SQL+参数直接返回缓存结果;执行 commit/rollback/close 后清空
  • 二级缓存(Global Cache):Mapper(Namespace)级别,跨 SqlSession 共享,需手动在 Mapper XML 中 <cache/> 开启,默认使用 PerpetualCache(HashMap)
  • 插件拦截:基于 JDK 动态代理拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四个接口的方法,通过 @Intercepts 和 @Signature 指定切入点
  • SQL 执行流程:SqlSession → Executor → StatementHandler → ParameterHandler(设参)→ JDBC execute → ResultSetHandler(结果映射)
  • 懒加载(Lazy Loading):通过 CGLIB/Javassist 代理结果对象,访问未加载属性时触发额外查询,需开启 aggressiveLazyLoading 和 lazyLoadingEnabled
详细解答

二级缓存一致性:MyBatis 的二级缓存仅在同一 Namespace 内有效,且默认不会感知其他 Namespace 或外部修改。多表关联查询时,若某个表在其他 Mapper 中被修改,二级缓存可能返回脏数据。建议仅对读多写少、数据变化不频繁的单表开启。

缓存淘汰策略:MyBatis 内置 FIFO、LRU、SOFT、WEAK 四种策略,默认 LRU。生产环境更推荐使用 Redis/Ehcache 等外部缓存作为二级缓存实现(实现 Cache 接口),获得更好的集群一致性。

插件原理:MyBatis 使用 Plugin.wrap 为目标对象创建代理,拦截器需实现 Interceptor 接口。多个插件会层层代理,形成代理链。执行顺序由 plugin 配置顺序决定,注意代理嵌套可能带来的性能损耗。

结果映射:自动映射(autoMapping)按列名与属性名匹配;嵌套结果(association/collection)支持嵌套查询和嵌套结果映射。嵌套结果映射通过结果集一次查询完成,性能优于嵌套查询的 N+1 问题。

工程实践

一级缓存在 Spring 事务中:由于 Spring 使用 SqlSessionTemplate 和线程绑定的 SqlSession,同一事务内多次查询会命中一级缓存。但事务结束后 SqlSession 关闭,缓存即失效。高并发下建议关闭二级缓存,改用 Redis 做应用层缓存,避免 MyBatis 本地缓存的内存压力和一致性问题。

高频追问
  • Q: MyBatis 与 JPA/Hibernate 的选型?A: MyBatis 灵活可控,适合复杂 SQL、遗留系统、DBA 深度参与的项目;JPA 开发效率高,适合领域驱动、标准 CRUD、对象关系映射复杂的场景。
  • Q: #{} 和 ${} 的区别?A: #{} 预编译参数(PreparedStatement),防注入;${} 直接字符串替换,用于动态表名/列名,有注入风险需严格校验。
MyBatis 缓存 插件 SQL
E04.06
Spring AOP 原理与动态代理:JDK 动态代理 vs CGLIB、Advisor 链、@Transactional 代理失效场景高级P2
核心要点
  • 核心概念:切面(Aspect)、连接点(JoinPoint)、切入点(Pointcut)、通知(Advice:@Before/@After/@Around/@AfterReturning/@AfterThrowing)、织入(Weaving)
  • JDK 动态代理:基于 java.lang.reflect.Proxy + InvocationHandler,要求目标实现接口,代理类实现同一接口
  • CGLIB:基于 ASM 字节码生成目标类子类,重写非 final 方法实现增强;Spring 5.x 起使用 repackaged 版(spring-core 内置)
  • Spring AOP 本质:运行时织入(代理模式),与 AspectJ 编译期/加载期织入(静态字节码增强)不同;Spring 默认仅支持方法级别连接点
  • 代理失效场景:自调用(this 调用内部方法)、final 方法、私有方法、静态方法、非 Spring 管理对象——@Transactional 失效的根因
详细解答

JDK 动态代理原理:Proxy.newProxyInstance(ClassLoader, interfaces, InvocationHandler) 在运行时生成代理类字节码,所有接口方法调用转发到 InvocationHandler.invoke();Spring 在 invoke 中执行 Advisor 链(多个通知按 @Order/Ordered 排序织入)。代理对象 getClass() 是 $Proxy0 而非目标类,类型转换只能到接口。

CGLIB 原理:通过继承目标类生成子类(如 Xxx$$EnhancerBySpringCGLIB),重写非 final 非私有方法,方法内插入拦截器调用链;构造时调用父类构造器,因此目标类必须有可访问的构造器(无参或参数匹配)。JDK 17+ 可通过 -Dspring.aop.proxytargetclass=false 或 proxyTargetClass=false 强制切回 JDK 代理。

Spring 选择逻辑:目标类实现接口 → 默认 JDK 动态代理;@EnableAspectJAutoProxy(proxyTargetClass=true) 或 CGLIB 库存在且目标无接口 → CGLIB。@Transactional 默认使用 AOP 代理实现,与 @Aspect 共用同一个 Advisor 链。

Advisor 链执行:Spring 将 @Aspect 方法解析为 Advisor(PointcutAdvisor),代理调用时按 @Order 排序组装 MethodInterceptor 责任链;@Around 包裹业务方法,@Before 在链首、@AfterReturning/@AfterThrowing 在链尾分支。

代理失效根因:Spring 注入的是代理对象,但类内部 this.method() 调用绕过代理直达目标类,增强全部失效。解决:注入自身代理(@Autowired 自身 / AopContext.currentProxy() 需 exposeProxy=true)、拆分为独立 Bean、或改用 TransactionTemplate 编程式事务。

工程实践

启动类加 @EnableAspectJAutoProxy(Spring Boot 自动装配已默认开启,spring.aop.auto=true);切面不要拦截自身(递归风险),通知内避免耗时操作(代理调用有额外开销,热点方法慎用 @Around);多切面用 @Order 控制顺序(值小优先,事务切面默认最低优先级 Ordered.LOWEST_PRECEDENCE,因此事务在外层、自定义切面在内层)。排查代理问题时:启动加 -Dspring.aop.proxytargetclass=false 对比行为;AopUtils.isAopProxy(bean) 判断是否代理;aopUtils.getTargetClass(bean) 取真实类。事务失效经典排查清单:自调用、非 public、final、异常被 catch 吞掉、异常类型不匹配(默认仅 RuntimeException)、类未被 Spring 管理、多线程内调用。

高频追问
  • Q: JDK 动态代理与 CGLIB 如何选择?A: 目标有接口且无需增强未实现接口的方法 → JDK(更轻、JDK 内置、原生支持);无接口/需要代理 final 之外的类方法/需要强类型子类 → CGLIB。Spring Boot 2.x+ 默认 proxyTargetClass=true 统一用 CGLIB(JDK 代理无法转换到具体类型)。
  • Q: Spring AOP 与 AspectJ 的区别?A: Spring AOP 运行时基于代理织入,仅方法级别、仅 Spring Bean;AspectJ 编译期/加载期字节码织入,支持字段/构造器/静态方法/控制流等更细粒度连接点,性能更高。Spring 项目默认用 Spring AOP,需 AspectJ 完整能力时用 @Configurable + load-time weaving。
  • Q: 为什么 @Transactional 内部调用会失效?A: 事务依赖代理对象拦截方法;this.method() 直接调用目标类方法不经代理,事务切面不执行。同理 @Async、@Cacheable、@Retryable 全部受影响。修复:注入自身代理、拆 Bean、AopContext.currentProxy() 或编程式事务。
  • Q: CGLIB 代理对 final 类/方法会怎样?A: final 类无法被继承、final 方法无法被重写,CGLIB 直接放弃增强(不报错但无效果)。因此被代理的类与方法避免 final;Spring 6/JDK 17 下 CGLIB 基于 ASM 仍可用,但接口多时优先 JDK 代理。
Spring AOP 动态代理 CGLIB 事务失效
E04.07
Agent / Tool Calling / 护栏:ReAct 循环、工具合约、步数熔断与幂等防资损专家P0
核心要点
  • Function Calling = 模型输出结构化工具意图;Agent = Brain(LLM) + Tools + Loop(感知-规划-行动-观察)多轮循环
  • ReAct:Thought → Action → Observation;Plan-and-Execute / 状态图(LangGraph)用结构约束自主性
  • 工具合约:清晰 description、参数最小化、幂等键、错误回填(勿抛异常打断循环)、超时与权限最小化
  • 生产护栏:max_steps、单工具次数上限、相同 (tool,args_hash) 循环检测、高危操作 HITL/沙箱预演
  • 与 C12.06/C12.07/C12.23、S12.03 互链:本章是原理速查,场景资损应急见 S12.03
详细解答

边界:单次 Function Calling 是「调一次工具」;Agent 是宿主循环执行 tool_call,直到模型声明完成或护栏终止。可靠性优先于自主性——能规则化的不要交给 LLM,能用固定状态图就不要纯自主 ReAct。

循环失败模式:① 工具错误被拼回上下文导致同参重试(资损高发);② 无进展步数耗尽 Token;③ 多 Agent 间接环(A→B→A)。治理:运行时护栏 + 工具侧幂等 + 编排层环检测。

工具合约要点:description 是选型唯一依据;查询可重试、写操作必须幂等(Idempotency-Key 绑意图而非纯参数);失败把错误当 Observation 回填,让模型改参或换工具。

分级自主:低风险查询用 RAG/规则;中风险用状态图 Agent;高风险(转账/删改)强制二次确认。记录每步 Thought/Action/Observation 便于审计。

工程实践

护栏默认值真源(以本卡为准):会话 max_steps=10、单工具 ≤3、总会话超时 120s;连续 2 次相同 (tool,args_hash) 直接终止转人工。金融写工具:simulate → 用户确认 → 真实执行。S12.03 讲资损应急与演练,数字若冲突以本卡为准。Spring AI 用 @Tool + ChatClient 循环,或接 MCP Client(见 E04.08)。

高频追问
  • Q: Agent 与 Function Calling 差在哪?A: FC 是模型能力(输出 tool_call JSON);Agent 是工程循环(多轮执行直到完成/熔断)。没有护栏的 Agent 等于无限 FC。
  • Q: 为何工具失败不能直接抛异常?A: 抛异常打断循环,模型无法改策略;应回填错误 Observation,由护栏限制重试次数。
  • Q: 幂等键为何不能只用金额+收款人?A: 「再转一笔相同金额」是新意图。应绑 request_id/意图 ID,同意图内去重、不同意图独立。
Agent Tool Calling 护栏 幂等
E04.08
MCP 协议速查:Client/Server、Tools/Resources/Prompts、与 Function Calling 分工架构P1
核心要点
  • MCP(Model Context Protocol)= AI 应用的「USB-C」:标准化工具/资源/Prompt 模板的发现与调用
  • 角色:MCP Server 提供能力;MCP Client(Agent/IDE)消费;JSON-RPC 2.0;传输 stdio 或 Streamable HTTP
  • 三类原语:Tools(可执行)、Resources(可读上下文)、Prompts(可复用模板)
  • 与 Function Calling:FC 决定「模型如何发起调用」;MCP 决定「工具如何被组织与跨进程复用」
  • 价值:M×N 集成降为 M+N;同一 Server 可被 Spring AI / Cursor / 多 Agent 复用。细节见 C12.07
详细解答

为何需要:进程内 @Tool 绑定应用,难以跨团队复用与独立扩缩。MCP 把「查库存/写工单」做成独立 Server,Client 运行时发现并调用。

安全边界:Server 侧鉴权、工具白名单、高危确认;Client 侧限制可连接 Server 集合;审计记录 tool 名与参数摘要。勿把 MCP 当免鉴权内网 RPC。

与 Agent 组合:Agent 护栏仍在 Client/编排层;MCP 只解决供给标准化。生产需同时有:发现、版本兼容、超时、幂等、可观测(span 打 tool 名)。

工程实践

本地开发用 stdio 子进程 Server;多租户/共享用 HTTP。工具 schema 变更走兼容策略(加可选字段优先)。Spring AI 作 MCP Client 时,把远程 Tools 映射进 ChatClient 工具集,仍套 E04.07 护栏。

发现与版本:生产用目录/注册中心暴露 Server 清单与版本区间;Client 声明可消费范围。破坏性变更用新 tool 名并行双跑一个弃用窗口,避免全链路解析失败。

Collector 式拓扑心智(对照可观测):边车/网关可做 MCP 入口鉴权与配额,业务 Server 只暴露工具;超时、并发上限、租户 ACL 放在入口,避免每个 Server 重复造轮子。细节见 C12.07 / S12.08。

高频追问
  • Q: 有了 MCP 还要不要进程内 Tool?A: 要。低延迟/强事务本地工具可进程内;跨团队复用、多运行时共享走 MCP。
  • Q: MCP 能否保证 Exactly-Once?A: 不能。协议层是调用通道;业务 Exactly-Once 靠工具幂等与编排层去重(同 E04.07)。
  • Q: stdio 与 HTTP 怎么选?A: 本地 IDE/单机用 stdio;多租户、跨主机、需鉴权审计用 Streamable HTTP。
  • Q: 工具 schema 加字段会不会炸 Client?A: 只增可选字段通常兼容;删改必填或改语义必须新版本/新名 + 弃用窗口。
MCP Tool Agent 协议

五、MySQL 与数据库

本组知识链

本组沿存储引擎正确性链:B+ 索引结构 → MVCC 可见性 → 慢查询优化闭环 → 复制与高可用拓扑。

  1. 索引结构:聚簇/二级/覆盖/最左前缀解释扫描成本。
  2. MVCC:Read View、undo 版本链、trx_id 可见性决定「幻读边界」。
  3. 慢查询:执行计划、失效模式、改写与覆盖索引形成闭环。
  4. 复制 HA:binlog、半同步/组复制、GTID、延迟治理与切换。
工程化要点
  • 索引变更与大表 DDL 走在线工具白名单与回滚脚本双人审。
  • 慢 SQL 必须有工单闭环:计划对比、回归、告警阈值。
  • 隔离级别与业务正确性用例写进验收,不只看默认 RR。
  • 主从延迟门禁:切读前检查可接受延迟与只读一致性。
  • 备份成功不等于可恢复——恢复演练进季度制度。
  • 扩展前先单机挖尽:分库分表是最后一步。
  • 大事务与长事务监控默认开启,防止 undo 膨胀。
本组思维模式
  • 成本在扫描行:先问访问路径再谈加机器。
  • 可见性语义:并发读写问题先用 MVCC 解释。
  • 计划优于感觉:无 EXPLAIN 的优化不进生产。
  • 复制有延迟:读己之写与切主窗口必须显式设计。
  • 可回滚优先:数据变更的第一问是如何退。
E05.01
MySQL InnoDB B+树索引:聚簇索引、二级索引、覆盖索引、最左前缀原理高级P0
核心要点
  • 聚簇索引(Clustered Index):叶子节点存储完整行数据,InnoDB 表必有且只有一个,按主键有序
  • 二级索引(Secondary Index):叶子节点存储索引列 + 主键值,查询需回表(除非覆盖索引)
  • 覆盖索引(Covering Index):查询所需列全在索引中,无需回表,Extra 显示 Using index
  • 最左前缀:联合索引 (a,b,c) 可被 (a)、(a,b)、(a,b,c) 使用,跳过前缀或范围后列失效
  • B+树特点:非叶子节点只存键值和指针(不存数据),叶子节点链表连接,范围查询高效
详细解答

InnoDB 的聚簇索引将数据与索引放一起,叶子节点就是数据页。主键索引即聚簇索引;若无显式主键,取第一个非空唯一索引;否则生成 6 字节的 row_id 隐藏主键。

二级索引查询流程:先在二级索引树找到主键值,再拿主键值去聚簇索引查完整行(回表)。若 SQL 只查索引列和主键,则二级索引叶子已有全部数据,无需回表,即覆盖索引。

最左前缀的底层原理:B+树按联合索引的第一列排序,第一列相同再按第二列排。查询条件如果不包含第一列,无法利用索引有序性,只能全表扫描。例如索引 (a,b,c):

  • WHERE a=1 AND b=2 AND c=3 — 完全使用
  • WHERE a=1 AND b=2 — 使用 a,b
  • WHERE a=1 — 使用 a
  • WHERE b=2 AND c=3 — 不使用(缺少 a)
  • WHERE a=1 AND c=3 — 只使用 a(b 缺失,c 无法跳过 b)
  • WHERE a>1 AND b=2 — 只使用 a(a 是范围,b 失效)

索引下推(ICP, Index Condition Pushdown):MySQL 5.6+ 将 WHERE 中索引列的判断下推到存储引擎层,减少回表次数。例如索引 (a,b),WHERE a='x' AND b LIKE '%y',ICP 会在引擎层过滤 b,不匹配的无需回表。

工程实践

EXPLAIN 分析时关注 type(至少 range)、key(实际使用的索引)、Extra(避免 Using filesort、Using temporary)。联合索引设计遵循最左前缀,将区分度高的、等值查询的列放前面。

高频追问
  • Q: 为什么 InnoDB 用 B+树而不用 B 树或 Hash?A: B+树非叶子不存数据,节点更小,IO 更少;叶子链表连接,范围查询高效。Hash 只支持等值,不支持范围和排序。
  • Q: 主键为什么要自增整数?A: 自增保证顺序插入,减少页分裂和碎片;整数占空间小,索引树更矮,查询更快。UUID 随机插入导致频繁页分裂。
MySQL B+树 索引优化 覆盖索引
E05.02
MySQL MVCC 实现:Read View 构造、undo log 版本链、trx_id 可见性判断高级P0
核心要点
  • MVCC(Multi-Version Concurrency Control):通过维护数据多版本实现读写不阻塞,读不加锁
  • 每行记录隐藏三个字段:DB_TRX_ID(最后修改事务 ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏主键)
  • undo log 形成版本链:每行修改前将旧值写入 undo log,通过 DB_ROLL_PTR 串联历史版本
  • Read View 是快照读时的可见性判断视图,包含 creator_trx_id、m_ids、min_trx_id、max_trx_id
  • RC 级别每次查询新建 Read View;RR 级别事务开始时创建,整个事务复用
详细解答

Read View 的四个核心字段:

  • creator_trx_id:创建该 Read View 的事务 ID
  • m_ids:生成 Read View 时活跃(未提交)的事务 ID 列表
  • min_trx_id:m_ids 中的最小值
  • max_trx_id:生成 Read View 时 InnoDB 将分配给下一个事务的 ID(即当前最大 trx_id + 1)

可见性判断规则(对某行记录的 DB_TRX_ID):

  1. DB_TRX_ID == creator_trx_id:自己修改的,可见
  2. DB_TRX_ID < min_trx_id:在 Read View 创建前已提交,可见
  3. DB_TRX_ID >= max_trx_id:在 Read View 创建后启动,不可见
  4. min_trx_id <= DB_TRX_ID < max_trx_id:若在 m_ids 中则未提交不可见;若不在则已提交可见

如果当前版本不可见,通过 DB_ROLL_PTR 沿 undo log 版本链回溯,找到第一个可见的历史版本。

RC 与 RR 的区别:RC 每次 SELECT 都新建 Read View,所以能看到其他事务已提交的最新修改;RR 事务开始时创建 Read View 并复用,保证事务内多次查询结果一致(快照读),但当前读(SELECT ... FOR UPDATE)仍会读到最新数据并加锁。

工程实践

RR 下 "幻读" 通过 MVCC + 间隙锁解决:快照读时 MVCC 保证不可见,当前读时间隙锁阻止插入。但 UPDATE ... WHERE id > 10 是当前读,可能触发幻读(其他事务插入 id=11 后,本事务再次更新会命中新行),这是 MySQL 的合理设计。

高频追问
  • Q: MVCC 能否完全解决幻读?A: 快照读(普通 SELECT)通过 MVCC 解决;但当前读(FOR UPDATE/LOCK IN SHARE MODE)使用间隙锁(Gap Lock)解决。若不使用当前读,RR 级别不会幻读。
  • Q: undo log 什么时候清理?A: 没有事务需要该历史版本时,由 purge 线程异步清理。长事务会阻止 undo log 清理,导致表空间膨胀和查询变慢。
MySQL MVCC Read View 事务隔离
E05.03
MySQL 慢查询优化:执行计划分析、索引失效、覆盖索引、SQL 改写高级P1
核心要点
  • 执行计划(EXPLAIN):关注 type(访问类型,至少 range)、key(使用的索引)、rows(扫描行数)、Extra(是否 Using filesort/Using temporary)
  • 索引失效场景:对列做函数运算、隐式类型转换、like '%xxx' 左模糊、or 条件一侧无索引、!=/<>/not in、is null(部分场景)
  • 覆盖索引(Covering Index):查询列全部在索引中,无需回表查聚簇索引,显著减少 IO。Extra 显示 Using index
  • 最左前缀原则:联合索引 (a,b,c) 只能用于 a / a,b / a,b,c 的查询顺序,跳过左侧列会导致索引失效
  • SQL 改写:分页深翻页用延迟关联(先查 ID 再 JOIN)、大表分页用覆盖索引子查询、避免 SELECT *、批量操作拆分为小事务
详细解答

EXPLAIN 的 type 从优到劣:system > const > eq_ref > ref > range > index > ALL。生产环境慢查询应至少达到 range,避免出现 ALL(全表扫描)。rows 是优化器估算的扫描行数,与实际可能有偏差,可通过 ANALYZE TABLE 更新统计信息。

隐式类型转换示例:字段 phone VARCHAR(11),查询 WHERE phone = 13800138000(数字),MySQL 会将字段转为 DOUBLE 再比较,导致索引失效。应保证字段类型与常量类型一致。

深分页优化:LIMIT 1000000, 10 需要扫描 1000010 行再丢弃前 1000000 条。优化方案:① 延迟关联 SELECT * FROM t JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000,10) tmp USING(id);② 记录上一页最大 ID WHERE id > last_id LIMIT 10。

覆盖索引的代价:索引列增多导致索引体积变大,维护成本增加。需权衡查询收益与写放大。通常对读多写少、查询列固定的大表效果显著。

工程实践

开启慢查询日志:slow_query_log=1,long_query_time=1。使用 pt-query-digest 分析慢日志。高并发系统建议用 Performance Schema 替代慢日志,减少磁盘开销。EXPLAIN ANALYZE(MySQL 8.0.18+)可查看实际执行时间和行数。

高频追问
  • Q: 为什么索引列区分度低时不建议走索引?A: 区分度低(如性别)回表代价高,优化器可能选择全表扫描。可通过复合索引将区分度高的列放前面,或结合覆盖索引避免回表。
  • Q: ORDER BY 如何优化?A: 尽量让 ORDER BY 列走索引(Using index 避免 filesort)。多列排序需满足最左前缀且同向(ASC/DESC)。无法走索引时,增大 sort_buffer_size 减少磁盘排序。
MySQL 慢查询 执行计划 索引优化
E05.04
MySQL 主从复制与高可用:binlog 复制链路、半同步/异步/组复制、GTID、主从延迟治理与 MHA/MGR 架构架构P1
核心要点
  • 复制链路:主库 binlog(ROW 格式为主)→ dump 线程推送 → 从库 IO 线程写 relay log → SQL 线程回放,三线程模型
  • 三种复制模式:异步复制(默认,主从不保一致)、半同步复制(semisync,主库等至少 1 个从库 ACK 再提交,AFTER_COMMIT/AFTER_SYNC)、组复制 MGR(Paxos 多数派,强一致/类 Raft)
  • GTID(Global Transaction ID):事务全局唯一(UUID:seq),从库断点续传不依赖 binlog 文件名+位点,自动跳过已执行事务,主从切换免手工定位
  • 并行复制:从库 SQL 线程多线程回放(replica_parallel_workers;旧名 slave_parallel_workers 自 8.0.26 起废弃。*_parallel_type 仅 LOGICAL_CLOCK 一种策略,该变量自 8.0.29 废弃、MySQL 9.5 起已移除),解决单线程回放瓶颈
  • 高可用架构:MHA(Manager 检测主故障,选新主 + 补 binlog + 虚拟 IP 漂移)、MGR(内置组复制自动选主)、Orchestrator(拓扑管理与故障切换编排)
详细解答

复制流程:客户端提交事务 → 主库写 binlog → dump 线程监听 binlog 变更并推送 → 从库 IO 线程 接收写入 relay log → 从库 SQL 线程(或并行回放线程组)顺序执行 relay log。主从状态看 SHOW SLAVE STATUS 的 Seconds_Behind_Master 与 Relay_Log_File/Pos。

半同步复制:主库提交事务前等待至少一个从库 ACK(8.4 起 rpl_semi_sync_source_enabled=1;旧名 rpl_semi_sync_master_enabled 已于 MySQL 9.5 随旧插件一并移除),事务丢失窗口从「网络传输 + 从库回放」压缩到「主库本地崩溃的极小窗口」;从库无 ACK 时半同步自动退化为异步(8.4 起 rpl_semi_sync_source_timeout) 控制等待时间),保证主库可用性。

GTID 复制:gtid_mode=ON + enforce_gtid_consistency=ON 后,事务带 GTID;从库通过 GTID_EXECUTED 集合判断哪些事务已执行,主从切换时新主只需 CHANGE MASTER TO ... MASTER_AUTO_POSITION=1 自动对齐,无需手工指定位点,杜绝「位点偏移重复执行」风险。

主从延迟治理:延迟来源——大事务(长 SQL 回放慢)、从库单线程、DDL 锁、主库压力大 binlog 生成快、从库硬件弱。方案:并行复制(replica_parallel_workers=4~16;8.0.26+ 推荐 replica_* 新名,MySQL 9.5 起 *_parallel_type 已移除)、拆分大事务(分批提交)、读写分离中间件按延迟剔除从库(seconds_behind_master > 阈值 摘除)、从库加索引/加硬件、pt-heartbeat 精准测延迟。

高可用切换:MHA 故障流程——Manager 通过 SSH 检查主库 → 找到 binlog 最全的从库晋升新主 → 补拉旧主残留 binlog → 其余从库 CHANGE MASTER 到新主 → VIP 漂移/更新注册中心。MGR 由 Paxos 组成员自动选主(单主模式写多节点),需 3 节点奇数 + 网络稳定,不适用于大事务/DDL 频繁场景。

工程实践

生产推荐:一主两从 + 半同步(8.4 起 rpl_semi_sync_source_enabled=1,旧名 rpl_semi_sync_master_enabled 9.5 起移除)+ GTID + 并行复制;读写分离用中间件(ShardingSphere/ProxySQL/MyCat)并做延迟感知路由;监控 Seconds_Behind_Master、relay log 堆积、IO/SQL 线程状态(YES/NO 中断即告警);大表 DDL 用 pt-online-schema-change 避免复制延迟;切换演练常态化(每季度 Kill 主库验证自动切换 < 30s)。从库一致性兜底:sync_binlog=1 + innodb_flush_log_at_trx_commit=1 保主库,从库可放宽;定期 pt-table-checksum + pt-table-sync 校验修复数据。

高频追问
  • Q: 主从延迟导致读写不一致怎么办?A: 三层:① 中间件按延迟摘除从库(延迟 > 阈值路由到主库);② 关键读强制主库(订单详情、支付结果等强一致场景 @Route(readOnly=false) 或 hint 直连主库);③ 业务容忍短时不一致(缓存写后过期 + 从库追平)。根治仍要并行复制 + 避免大事务。
  • Q: 半同步复制与组复制(MGR)怎么选?A: 半同步 + MHA 适合大多数业务(部署简单、切换可控、事务性能损耗小);MGR 适合对一致性要求极高、节点数固定(3/5/7)且网络稳定的场景,代价是写性能下降(多数派确认)与运维复杂度;金融核心可用 MGR 单主模式,非核心用半同步。
  • Q: GTID 复制有什么限制?A: 临时表跨事务会报错(GTID 一致性约束)、CREATE TABLE ... SELECT 被禁止(可拆两步)、非事务引擎(MyISAM)不支持、升级切换需先关闭 GTID 再开启。生产从建库就启用 GTID 最省心。
  • Q: 主库宕机后如何保证不丢数据?A: 没有绝对零丢失。半同步把丢失窗口压到最小(主库崩溃瞬间已提交未推送的极少事务);再叠加:主库 sync_binlog=1 + innodb_flush_log_at_trx_commit=1(双 1 配置,崩溃不丢已提交事务)、MHA 切换时从旧主拉取残留 binlog 补传;极端场景(机房级故障)靠跨机房复制 + 定期备份(备份 + binlog 可恢复到任意时间点)。
MySQL 主从复制 高可用 binlog

六、Redis 与缓存

本组知识链

本组把缓存从「快」拉回「正确且可降级」:一致性模式 → 分布式锁争议 → 持久化选型 → 三大问题与热/大 key → Pipeline/Lua 边界。

  1. 一致性:Cache Aside、延迟双删、Canal 订阅的窗口与失败语义。
  2. 分布式锁:Redisson 看门狗 vs RedLock 的正确性与取舍。
  3. 持久化:RDB/AOF/混合与重写对 RPO 的含义。
  4. 稳态治理:穿透击穿雪崩、热/大 key、Pipeline/事务/Lua 与 Cluster 限制。
工程化要点
  • 一致性窗口写进接口契约与对账任务,禁止口头「最终一致」。
  • 缓存失败必须可降级到可承受的回源配额。
  • 热 key/大 key 有识别看板与拆分/本地缓存方案。
  • 击穿穿透雪崩演练进大促清单。
  • 锁的持有时间、续期与 fencing token 写进评审。
  • 持久化与复制切换 Runbook 可演练。
  • Cluster 下 Lua/事务跨槽限制进入开发规范。
本组思维模式
  • 加速有代价:缓存是一致性与可用性的交易。
  • 锁不是银弹:超卖问题优先状态机与对账。
  • 分型治理:穿透/击穿/雪崩手段不同,禁止一把梭。
  • 容量一体:回源配额与缓存命中率同模型。
  • 原子性边界:Pipeline≠事务;Lua 有槽与时效限制。
E06.01
Redis 缓存一致性:Cache Aside、延迟双删、Canal 订阅 binlog架构P0
核心要点
  • Cache Aside(旁路缓存):读时先读缓存,miss 则读 DB 回写缓存;写时先更新 DB,再删缓存
  • 延迟双删:先删缓存 → 更新 DB → 延迟(如 500ms)再删缓存,解决并发读写导致的数据不一致
  • Canal 订阅 binlog:异步监听 MySQL binlog,数据变更后自动刷新/删除缓存,实现最终一致
  • 为什么删除而非更新缓存:删除简单幂等,更新可能覆盖其他并发写入的新值
  • 强一致性需引入分布式锁或读写串行化,性能代价大,多数业务接受最终一致
详细解答

Cache Aside 是最常用的缓存策略。写操作选择"删缓存"而非"更新缓存"的原因是:并发场景下,两个写线程同时更新同一数据,若采用更新缓存,后执行的线程可能因网络延迟反而先到达缓存,导致缓存值被旧值覆盖(即"写覆盖")。删除缓存则简单幂等,下次读时从 DB 加载最新值。

但 Cache Aside 仍有短暂不一致窗口:线程 A 更新 DB 后删除缓存前,线程 B 读缓存 miss 后从 DB 读到旧值并回写缓存。延迟双删通过第二次删除来缩短这个窗口。

Canal 方案原理:Canal 伪装成 MySQL Slave,订阅主库的 binlog,解析出数据变更事件(INSERT/UPDATE/DELETE),投递到 MQ(如 RocketMQ/Kafka),消费者异步删除对应缓存。优势:

  • 业务代码无侵入,缓存逻辑与业务解耦
  • binlog 是最终的数据变更来源,不会遗漏
  • 天然支持分布式事务的最终一致性

Canal 的延迟通常在毫秒到秒级,适合容忍短暂不一致的业务。金融级强一致场景需配合分布式锁(如 Redisson)实现读写串行化。

工程实践

延迟双删的延迟时间应大于主从同步延迟 + 业务读操作耗时。生产环境推荐 Canal + MQ 方案,配合本地缓存(Caffeine)+ Redis 二级缓存,本地缓存设置短 TTL(如 30s)作为兜底。

高频追问
  • Q: 先删缓存再更新 DB 有什么问题?A: 删缓存后、更新 DB 前,其他线程读缓存 miss 从 DB 读到旧值并回写缓存,导致缓存长期为旧值。概率比"先更新 DB 再删缓存"的不一致更大。
  • Q: Canal 订阅 binlog 的延迟如何优化?A: 1) Canal Server 与 MySQL 同机房部署;2) 使用 MQ 的批量消费;3) 热点数据变更合并去重;4) 本地缓存兜底,容忍秒级不一致。
Redis 缓存一致性 Canal 最终一致
E06.02
Redis 分布式锁:Redisson 看门狗机制 vs RedLock 的争议与取舍架构P1
核心要点
  • 基础分布式锁:SET key value NX EX 30,原子性设置键和过期时间,避免死锁
  • Redisson 看门狗:加锁成功后启动定时任务,每隔 lockWatchdogTimeout/3(默认 10s)续期,直到业务释放锁
  • RedLock(Redis Redlock):在 N 个独立 Redis 节点上同时加锁,过半成功则认为获取锁,释放时向所有节点发送解锁
  • RedLock 争议:Martin Kleppmann 指出时钟漂移、网络延迟会导致多个客户端同时认为自己持有锁
  • 生产推荐:单机 Redis 用 Redisson 看门狗;多主环境用 RedLock 需谨慎,或改用 ZooKeeper/Etcd 实现 CP 锁
详细解答

Redisson 的看门狗机制解决了"业务执行时间超过锁过期时间"的问题。默认锁超时 30s,看门狗每隔 10s 检查锁是否仍被当前线程持有,若是则续期到 30s。业务代码通过 lock.lock() 获取锁,通过 lock.unlock() 释放;若业务异常退出未释放,锁到期自动释放。

RedLock 算法步骤:

  1. 获取当前时间戳 T1
  2. 依次向 N 个(通常 5 个)独立 Redis 节点发送 SET key NX EX
  3. 计算获取锁总耗时 T2 - T1,若成功节点数 > N/2 且耗时 < 锁过期时间,则获取成功
  4. 锁实际过期时间 = 设定过期时间 - 获取耗时
  5. 释放时向所有节点发送 DEL

Martin Kleppmann 的质疑核心:RedLock 依赖系统时钟,若某节点时钟向前跳跃(如 NTP 同步),锁可能提前过期;且网络延迟可能导致客户端 A 在节点 1 获取锁后过期,客户端 B 在节点 1 获取新锁,但客户端 A 仍认为持有其他节点的锁。

实际生产中,Redis 主从异步复制场景下,主节点宕机可能丢失锁数据,从节点晋升为主后,其他客户端可再次获取同一锁。antirez 的 RedLock 与 Redisson 历史封装 RedissonRedLock 的获锁条件都是过半节点成功(N/2+1),不是「等全部节点加锁成功」;释放时再向全部节点尝试 DEL。即便多数派成功,仍受时钟漂移、GC pause 与网络延迟影响(Kleppmann 批判)。较新 Redisson 版本已将 RedLock 封装标为废弃,需按所用版本核对;正确性敏感场景优先 ZooKeeper/etcd + fencing token,可容忍极端失效则用单实例 Redisson 看门狗 + 业务幂等兜底。

工程实践

推荐 Redisson 单实例 + 看门狗模式,配合唯一 value(UUID+线程 ID),释放时 Lua 脚本校验 value 再删除,防止误删他人锁。高可用场景用 Redis Cluster 或哨兵,但需接受极端情况下的锁重入可能。

高频追问
  • Q: 为什么 Redisson 的看门狗续期是 10s 而不是 30s?A: 默认锁有效期 30s,看门狗每隔 10s(1/3)续期,确保即使某次续期失败,还有 20s 的缓冲时间进行下一次续期,避免业务执行中锁过期。
  • Q: ZooKeeper 分布式锁相比 Redis 的优势?A: ZK 是 CP 系统,通过临时顺序节点实现锁,节点宕机自动释放,无时钟依赖。但性能低于 Redis,适合并发量不高但强一致性要求高的场景。
Redis 分布式锁 Redisson RedLock
E06.03
Redis 持久化:RDB 快照、AOF 日志、混合模式、重写机制与选型高级P1
核心要点
  • RDB:定时 fork 子进程生成内存快照二进制文件,恢复速度快,但两次快照间数据可能丢失,适合灾难恢复
  • AOF:记录每条写命令到日志文件,默认每秒 fsync,数据安全性高但文件体积大、恢复速度慢
  • 混合模式(Redis 4.0+):AOF 重写时先写 RDB 全量数据,再追加增量 AOF,兼顾恢复速度和数据安全
  • AOF 重写(BGREWRITEAOF):fork 子进程遍历内存数据生成新 AOF,不阻塞主进程;重写期间新命令写入 AOF 重写缓冲区,结束后追加到新文件
  • Copy-On-Write:fork 后父子进程共享内存页,子进程写快照时父进程修改的页会复制一份,因此大内存下 fork 可能耗时且导致 OOM
详细解答

RDB 触发条件:手动(SAVE/BGSAVE)或自动(配置 save 900 1 等)。BGSAVE 通过 fork 子进程实现,父进程继续处理请求。RDB 文件是紧凑的二进制格式,适合全量备份和传输(如主从复制初次同步)。

AOF 的三种刷盘策略:always(每条命令 fsync,最安全但最慢)、everysec(每秒 fsync,默认,最多丢 1s 数据)、no(由 OS 决定刷盘,最快但最不安全)。生产环境推荐 everysec。

AOF 重写触发:自动(aof-current-size / aof-base-size 超过指定百分比)或手动(BGREWRITEAOF)。重写期间 Redis 将新命令同时写入旧 AOF 和 AOF 重写缓冲区,子进程结束后父进程将缓冲区内容追加到新 AOF 并原子替换旧文件。

混合持久化配置:aof-use-rdb-preamble yes。重写后的 AOF 文件前半部分是 RDB 格式,后半部分是 AOF 格式,恢复时先加载 RDB 再重放 AOF,速度接近纯 RDB,安全性接近纯 AOF。

工程实践

生产环境推荐同时开启 RDB + AOF(混合模式),RDB 用于快速恢复和备份,AOF 用于数据安全。避免在高峰时段自动触发重写,可通过 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 调整阈值。大内存实例(>10GB)fork 可能耗时数百毫秒,建议升级硬件或使用 Redis 7.0 的多 part AOF 减少重写开销。

高频追问
  • Q: 主从复制时从节点加载 RDB 期间数据如何处理?A: Redis 7.0 前从节点加载 RDB 期间阻塞所有请求;7.0+ 支持多 part RDB 和无盘复制(diskless replication),可逐步加载减少阻塞时间。
  • Q: 为什么 fork 会导致内存翻倍?A: fork 时 OS 采用 COW,理论上共享内存。但父进程持续写命令会触发大量页复制,若写流量大,实际内存占用可能接近 2 倍。需预留足够内存并监控。
Redis RDB AOF 持久化
E06.04
缓存穿透 / 击穿 / 雪崩:定义边界、防护组合与容量门禁架构P1
核心要点
  • 穿透:查询不存在的数据,缓存与 DB 都 miss,恶意/脏流量打穿 DB
  • 击穿:热点 key 过期瞬间大量并发回源,单 key 打爆 DB
  • 雪崩:大量 key 同时过期或缓存集群故障,回源洪峰
  • 防护组合:布隆/空值缓存(穿透)、互斥锁或逻辑过期(击穿)、随机 TTL + 多级缓存/熔断(雪崩)
  • 深挖篇章:C08.03 / 场景 S03.04;一致性见 E06.01
详细解答

三类问题常被混称,工程表达须先划清边界:穿透是「根本无数据」;击穿是「有数据但热点过期」;雪崩是「大面积失效或缓存不可用」。对策不可互换——布隆挡不住雪崩,随机 TTL 也解决不了穿透。

穿透:布隆/Cuckoo 拦截确定不存在;短 TTL 空值缓存防枚举;网关限流脏 key。

击穿:单飞(mutex)或逻辑过期 + 异步重建;热点可永不过期 + 主动刷新。

雪崩:TTL 加抖动;多级缓存;缓存故障时熔断降级与预案(本地兜底/只读)。

工程实践

上线门禁:热点 key 清单、空值策略、TTL 抖动范围、缓存故障演练。指标:回源 QPS、布隆拒绝率、单 key 并发重建次数。与 C08.03 对照答题,避免只背名词。

高频追问
  • Q: 空值缓存会不会撑爆 Redis?A: 必须短 TTL + 体积上限;枚举攻击还要布隆/网关。
  • Q: 逻辑过期和互斥哪个好?A: 读多写少可用逻辑过期保可用性;强一致读偏好互斥单飞。
穿透 击穿 雪崩 Redis
E06.05
热 key / 大 key:识别、拆分、本地缓存与集群倾斜治理架构P1
核心要点
  • 热 key:单 key QPS 过高导致 CPU/带宽/槽位倾斜
  • 大 key:单 value 过大(大 string/hash/list/zset),阻塞或复制风暴
  • 治理:拆 key、本地缓存、读写分离、压缩与分片结构
  • Cluster:热 key 不会因槽迁移自动分散,需业务层 hash-tag/副本读
  • 深挖:C08 集群与数据结构篇;监控用 redis-cli / 代理热点统计
详细解答

热 key:发现靠代理/客户端打点或 Redis 7+ 监控;治理优先本地缓存(Caffeine)+ 多副本只读,其次拆成 N 个子 key 随机读写。

大 key:string 过大改拆分或外置对象存储;hash/list/zset 控制 field 数与扫描批量;删除用 UNLINK/分批,避免阻塞。

Cluster 下大 key 迁移成本高,上线前做 key 体积扫描门禁。

工程实践

门禁:单 key 体积与 QPS 阈值;发布前 hot-key 扫描。告警:单槽 CPU、slowlog、网络出带宽。删大 key 走异步与低峰。

高频追问
  • Q: 热 key 加本地缓存如何保证一致?A: 短 TTL + 变更总线失效;接受短暂不一致或用版本号。
  • Q: 为何 CLUSTER 不能自动打散热 key?A: 路由由 key 哈希决定,同一 key 永远同一槽。
热key 大key Cluster
E06.06
Redis Pipeline / 事务 / Lua:批量化、原子性边界与 Cluster 限制高级P2
核心要点
  • Pipeline:打包命令减 RTT,非原子,中间可被插队
  • MULTI/EXEC:单机队列原子执行,无回滚;Cluster 跨槽禁止
  • Lua:服务端脚本原子,可替代轻量事务;注意超时与写扩散
  • 与 Redisson 锁脚本:校验 value 再 DEL,防误删
  • 深挖:E06.02 锁;生产禁超长 Lua 阻塞事件循环
详细解答

Pipeline 优化延迟,不提供隔离;失败需业务幂等。

事务 Redis 的 MULTI 不是关系库事务:无回滚,WATCH 做乐观锁。

Lua 适合短临界区(锁释放、限流计数);Cluster 要求脚本访问的 key 同槽(hash-tag)。

工程实践

批量读用 Pipeline;跨槽改并发单命令或业务拆分。Lua 设超时监控,禁止在脚本里做外部 IO。

高频追问
  • Q: Pipeline 能保证全成功吗?A: 不能;部分失败要按回复逐条处理。
  • Q: 为何锁释放用 Lua?A: GET+DEL 非原子;脚本内校验 value 再删防误删。
Pipeline Lua 事务

七、分布式与微服务

本组知识链

本组覆盖分布式协作主链:事务模式对比 → 熔断限流 → 网关/注册配置 → RPC 与容错 → 高并发架构拼图 → Outbox/CDC 端到端。

  1. 分布式事务:2PC/TCC/SAGA/Seata AT·TCC 的隔离与性能边界。
  2. 流量治理:令牌桶/漏桶/滑动窗口、Sentinel 自适应与热点限流。
  3. 云原生底座:Gateway 路由过滤、Nacos 一致协议、Dubbo SPI 与集群容错。
  4. 可靠投递:Outbox/CDC 同库落箱、幂等投递与对账闭环。
工程化要点
  • 事务模式选型表强制入库:业务隔离需求→推荐模式→回滚/空回滚治理。
  • 超时预算可加总;治理参数变更可回滚。
  • 熔断半开观察窗与探测流量上限写进配置门禁。
  • 网关限流与后端限流职责划分清晰,避免双重误杀。
  • 配置热更新有双实例与线程池风险验收。
  • Outbox/CDC 必须有对账 SLA 与告警。
  • 服务雪崩演练:依赖抖动下的降级与舱壁验证。
本组思维模式
  • 本地事务优先:跨服务事务是最后手段。
  • 预算化超时:链路超时必须可加总到用户 SLA。
  • 对账兜底:异步与最终一致靠对账而不是信念。
  • 治理可回滚:限流熔断参数是生产变更。
  • 故障域显式:舱壁与依赖拓扑先于加机器。
E07.01
分布式事务:2PC/TCC/SAGA/Seata AT 原理对比与生产选型架构P0
核心要点
  • 2PC(两阶段提交):准备阶段锁定资源 + 提交阶段确认,同步阻塞、单点故障、数据不一致风险
  • TCC(Try-Confirm-Cancel):业务层实现预留/确认/取消三接口,无全局锁、性能高、业务侵入大
  • SAGA:长事务拆分为本地事务序列,每个有对应补偿操作,正向成功则完成,失败则逆序补偿
  • Seata AT:自动代理数据源,拦截 SQL 生成前后镜像,失败时根据镜像回滚,对业务零侵入
  • 生产选型:强一致用 XA/2PC,最终一致用 Seata AT/SAGA/TCC,高并发场景优先异步 SAGA + MQ
详细解答

2PC 是 XA 规范的基础。协调者(TM)先询问所有参与者(RM)是否能提交(Prepare),全部 Yes 后发送 Commit;任一 No 则发送 Rollback。问题:准备阶段参与者需锁定资源(如数据库行锁),其他事务阻塞;协调者单点故障可能导致参与者长期持有锁;网络分区时部分参与者提交、部分回滚,数据不一致。

TCC 将事务控制上移到业务层:

  • Try:预留资源(如冻结库存、预扣余额)
  • Confirm:确认执行,使用预留资源
  • Cancel:释放预留资源

TCC 无全局锁,性能远高于 2PC,但需要业务代码实现三套逻辑,开发成本高。幂等性、空回滚、悬挂问题需额外处理。

Seata AT 是目前 Java 生态最常用的分布式事务方案。原理:

  1. Seata 代理数据源(DataSourceProxy),拦截业务 SQL
  2. 执行前查询并保存前镜像(Before Image)
  3. 执行业务 SQL
  4. 查询并保存后镜像(After Image)
  5. 向 TC(Transaction Coordinator)注册分支事务
  6. 全局提交时异步删除镜像;全局回滚时用前镜像生成逆向 SQL 执行回滚

AT 模式对业务零侵入,但回滚依赖镜像,复杂 SQL(如批量更新、JOIN 更新)的镜像生成可能不准确。

工程实践

电商下单场景推荐 SAGA + MQ:创建订单(本地事务)→ 发送扣库存消息 → 库存服务扣减 → 发送扣款消息 → 支付服务扣款。任一环节失败发送补偿消息逆序回滚。高并发下比 AT 性能高一个数量级。

高频追问
  • Q: Seata AT 的回滚是物理回滚还是逻辑回滚?A: 逻辑回滚。根据前镜像生成逆向 UPDATE/DELETE/INSERT SQL 执行,不是数据库的物理 undo。因此若业务 SQL 修改了非索引列,镜像可能不全,回滚有风险。
  • Q: TCC 的悬挂、幂等、空回滚如何解决?A: 悬挂:Try 超时后执行了 Cancel,之后 Try 又到达,需在 Cancel 时记录事务状态,Try 时检查。幂等:Confirm/Cancel 操作幂等设计(数据库唯一索引)。空回滚:Try 未执行时收到 Cancel,直接返回成功。
分布式事务 Seata SAGA TCC
E07.02
熔断限流算法:令牌桶、漏桶、滑动窗口、Sentinel 自适应限流高级P1
核心要点
  • 令牌桶:以固定速率往桶里放令牌,请求需获取令牌才能执行,可应对突发流量(桶有容量)
  • 漏桶:请求像水一样先进入桶,以固定速率流出,平滑突发流量,但无法应对突发(桶满则拒绝)
  • 滑动窗口:将时间划分为多个小窗口,统计最近 N 个窗口的请求数,比固定窗口更精确,避免边界突刺
  • Sentinel 自适应:基于系统负载(CPU、RT、并发线程数)动态调整阈值,而非固定 QPS
  • 熔断三种状态:关闭(正常)→ 打开(拒绝)→ 半开(探测),基于错误率/慢调用比例触发
详细解答

令牌桶和漏桶的差异:

  • 令牌桶允许一定程度的突发:若桶中有 100 个令牌,瞬间 100 个请求都能通过,之后受限于产生速率
  • 漏桶严格限制流出速率:即使瞬间大量请求进入,也只能以固定速率处理,桶满后新请求被拒绝

滑动窗口限流将 1 分钟分为 6 个 10s 窗口,每次统计当前窗口和前 5 个窗口的请求总数。相比固定窗口,滑动窗口不会在窗口边界出现 2 倍流量的突刺(如固定窗口在 59s 和 0s 各允许 100 请求,实际 1s 内通过了 200)。

Sentinel 的熔断策略:

  • 慢调用比例:当慢调用(RT > 阈值)占比超过比例,触发熔断
  • 异常比例:当异常请求占比超过比例,触发熔断
  • 异常数:当异常数超过阈值,触发熔断

Sentinel 的自适应限流(System Rule)基于系统指标:当系统负载(CPU、Load)高时自动降级,保护系统不被压垮。这与固定 QPS 限流不同,后者在系统能力强时可能浪费资源,在系统负载高时可能无法及时保护。

工程实践

API 网关层用令牌桶做全局限流(如 Redis + Lua 实现分布式令牌桶),微服务内部用 Sentinel 做细粒度限流和熔断。熔断半开状态的探测请求数设置为 5~10,成功后逐步恢复,避免瞬时全量放开。

高频追问
  • Q: 令牌桶和漏桶在分布式环境下如何实现?A: 分布式令牌桶可用 Redis + Lua 原子脚本实现(GET/INCR/EXPIRE),或基于 Redisson 的 RRateLimiter。漏桶可用消息队列(MQ)实现,请求入队,消费者固定速率处理。
  • Q: Sentinel 的滑动窗口用的是什么数据结构?A: 基于 LeapArray 的环形数组,每个元素是一个时间窗口(WindowWrap),统计该窗口内的请求数、通过数、拒绝数等。新请求根据当前时间计算窗口索引,O(1) 更新。
熔断限流 Sentinel 令牌桶 滑动窗口
E07.03
Spring Cloud Gateway:路由、断言、过滤器、限流与自适应熔断架构P1
核心要点
  • 基于 Spring 5 + WebFlux + Reactor 构建,采用异步非阻塞 IO,相比 Zuul 1.x 的阻塞模型吞吐量更高
  • 路由(Route):由 ID、目标 URI、断言(Predicate)集合、过滤器(Filter)集合组成,断言匹配请求,过滤器处理请求/响应
  • 内置断言:Path、Method、Header、Query、Cookie、Before/After/Between 时间等,支持自定义断言
  • 网关过滤器:RewritePath、AddRequestHeader、CircuitBreaker(集成 Resilience4j)、RequestRateLimiter(基于 Redis 限流)
  • 全局过滤器(Global Filter):LoadBalancerClient(负载均衡)、NettyRoutingFilter(转发请求)、ReactiveLoadBalancerClientFilter
详细解答

Spring Cloud Gateway 的核心处理链:DispatcherHandler → RoutePredicateHandlerMapping → FilteringWebHandler → GatewayFilterChain。请求到达后先匹配路由断言,命中后按 Ordered 排序执行过滤器链。

限流实现依赖 RequestRateLimiter 过滤器,默认集成 Redis + Lua 脚本实现令牌桶算法。KeyResolver 可基于 IP、用户、接口维度限流。配置示例:redis-rate-limiter.replenishRate=10(每秒补充 10 个令牌),burstCapacity=20(桶容量 20)。

熔断通过集成 Resilience4j 或 Spring Cloud Circuit Breaker 实现。当下游服务异常比例超过阈值时,网关直接返回降级响应,避免雪崩。支持慢调用比例、异常比例、异常数三种策略。

性能优化:开启 HTTPS 会话复用、合理配置 Netty 的 worker 线程数(默认 CPU 核数 ×2)、开启响应压缩、使用直连路由减少 LB 开销。大流量场景建议网关前置 Nginx 做 SSL 卸载和静态缓存。

工程实践

生产环境网关建议独立部署,与业务服务解耦。配置动态路由可结合 Nacos Config 或 Spring Cloud Bus 实现热更新。链路追踪集成 Sleuth + Zipkin,在全局过滤器中提取/注入 Trace ID。注意 WebFlux 的线程模型,避免在过滤器中执行阻塞操作(如 JDBC),应使用 Reactive 客户端。

高频追问
  • Q: Gateway 与 Nginx 的选型与协作?A: Nginx 擅长七层负载、静态资源、SSL 卸载;Gateway 擅长动态路由、服务发现、熔断限流。典型架构:Nginx → Gateway → 微服务。
  • Q: 如何实现灰度发布?A: 自定义 Predicate 读取 Header/Cookie 中的版本标签,结合元数据(metadata)路由到指定版本实例。或基于 Nacos 权重+元数据实现。
Spring Cloud Gateway 限流 熔断
E07.04
Nacos:注册中心服务发现、配置中心动态刷新、一致性协议高级P1
核心要点
  • 注册中心:服务提供者启动时注册元数据(IP、端口、权重、元数据),消费者订阅后本地缓存服务列表,支持推模式(Push)和拉模式(Pull)
  • 配置中心:支持 Namespaces(环境隔离)、Groups(分组)、Data ID 三级维度,配置变更通过长轮询(Long Polling)实时推送到客户端
  • 一致性协议:Nacos 2.x 默认使用 JRaft 实现 CP 模式的服务注册;配置管理使用 Distro 协议(AP 模式),牺牲强一致性换取高可用
  • 服务健康检查:临时实例(ephemeral=true)由客户端主动发送心跳(5s 间隔),15s 未收到标记不健康,30s 剔除;持久化实例由服务端主动探测
  • 负载均衡:集成 Spring Cloud LoadBalancer(Ribbon 已在 Spring Cloud 2020.0 移除,仅作演进对比),支持随机、轮询、权重、最少活跃调用数等策略
详细解答

Nacos 2.x 相比 1.x 的重大改进是引入了 gRPC 长连接,替代了 HTTP 短轮询,大幅降低了注册中心的 CPU 和连接压力。每个客户端与 Nacos 服务端保持一个 gRPC 连接,注册/订阅/心跳都在该连接上复用。

配置中心的长轮询机制:客户端每 30s 发起一次请求,服务端持有请求 29.5s,期间若配置变更立即响应;无变更则 30s 后返回空。这样兼顾了实时性和服务端压力。

服务发现的本地缓存:客户端通过 ServiceInfoHolder 维护本地服务列表,服务端推送变更后增量更新。若 Nacos 全挂,客户端仍可使用本地缓存继续调用,但无法感知新实例上下线。

集群部署建议:至少 3 节点,MySQL 做配置持久化,Raft 协议保证注册服务元数据的强一致性。注册中心与配置中心可分离部署,避免互相影响。

工程实践

Nacos 2.x 推荐配置:开启权限认证(nacos.core.auth.enabled=true),修改默认命名空间和密码。Spring Cloud 中配置刷新需配合 @RefreshScope 或 @ConfigurationProperties。大数据量配置建议拆分多个 Data ID,避免单配置过大导致推送延迟。

高频追问
  • Q: Nacos 与 Eureka 的核心区别?A: Nacos 支持 CP/AP 切换、配置中心、长连接推送;Eureka 仅 AP、仅注册中心、HTTP 轮询。Nacos 2.x 性能显著优于 Eureka。Eureka 2.x 已停更(Netflix 2018 终止开发)、1.x 仅维护模式,新架构首选 Nacos。
  • Q: 服务注册不上如何排查?A: 检查客户端日志确认是否连接成功;检查 Nacos 服务端是否开启认证;检查网络是否通(gRPC 端口 9848/9849);查看服务端 cluster.conf 是否正确。
Nacos 服务发现 配置中心 Raft
E07.05
Seata AT 模式: undo_log、全局锁、二阶段提交、隔离级别与性能架构P1
核心要点
  • AT 模式基于二阶段提交(2PC)演进,一阶段执行业务 SQL 并记录 undo_log(前置镜像),二阶段成功则删除 undo_log,失败则用 undo_log 回滚
  • 全局事务由 TC(Transaction Coordinator)协调,TM(Transaction Manager)发起,RM(Resource Manager)管理分支事务
  • 全局锁:一阶段提交前申请全局行锁,防止脏写;本地事务提交后立即释放本地锁,但全局锁持有至二阶段结束,降低并发度
  • 隔离级别:默认读未提交,通过 SELECT FOR UPDATE 代理实现读已提交;全局锁保证写隔离,避免脏写
  • TC 高可用:基于 DB 或 Redis 存储全局事务会话,TC 集群通过共享存储实现无状态化
详细解答

AT 模式的一阶段流程:RM 拦截业务 SQL,解析生成 undo_log(JSON 格式记录修改前后的行数据),提交本地事务(此时数据已真实写入)。二阶段异步执行:若全局事务提交,TC 通知 RM 删除 undo_log;若回滚,RM 用 undo_log 生成反向 SQL 补偿。

全局锁表(lock_table)存储在 TC 侧,key 为表名+主键。一阶段提交前 RM 向 TC 申请锁,TC 检查是否冲突。若冲突,当前事务等待或重试。由于本地事务已提交,其他非 Seata 事务可能读到中间状态,因此默认隔离级别为读未提交。

回滚失败处理:若二阶段回滚时发现数据已被外部修改(与前置镜像不一致),则进入人工介入状态,不再自动回滚,防止数据错乱。

性能损耗主要来自:SQL 解析( Druid SQL Parser)、undo_log 写入、全局锁网络交互。官方测试显示 AT 模式相比无事务损耗约 10%~20%,适用于绝大多数 OLTP 场景。

工程实践

AT 模式要求业务表必须有主键,且不支持多表外键约束。undo_log 表需在每个业务库创建。高并发场景建议开启 TC 的 DB 批量处理和网络压缩。若对隔离级别要求极高,可考虑 Seata XA 模式(强一致性但阻塞)或 Saga 模式(长事务、最终一致)。

高频追问
  • Q: AT 与 TCC 模式如何选型?A: AT 模式无业务侵入,自动解析 SQL,适合通用 CRUD;TCC 需手动实现 Try/Confirm/Cancel,性能更好、控制更精细,适合复杂业务和高并发扣减。
  • Q: 全局事务挂起/超时如何处理?A: TC 可配置全局事务超时时间(默认 60s),超时后自动驱动回滚。需确保业务操作幂等,避免回滚后重试导致数据不一致。
Seata 分布式事务 AT 模式 2PC
E07.06
Dubbo:服务导出/引用、SPI 扩展、负载均衡、集群容错与线程模型高级P1
核心要点
  • 服务导出(Export):Spring 启动时解析 @DubboService 或 XML,创建 Protocol(如 DubboProtocol)并开启 NetServer(默认 Netty)监听端口,将服务注册到注册中心
  • 服务引用(Refer):@DubboReference 触发创建代理对象(ProxyFactory),订阅注册中心服务列表,维护 Invoker 调用链
  • SPI 机制:基于 JDK SPI 增强,支持 Key-Value 映射、AOP 包装(Wrapper)、自适应扩展(Adaptive),配置文件为 META-INF/dubbo/ 目录下
  • 负载均衡:Random(默认加权随机)、RoundRobin(加权轮询)、LeastActive(最少活跃调用)、ConsistentHash(一致性哈希)
  • 集群容错:Failover(默认重试 2 次)、Failfast(快速失败)、Failsafe(忽略异常)、Failback(失败定时重试)、Forking(并行调用多实例)
详细解答

Dubbo 3.x 引入应用级服务发现(接口级 → 应用级),注册中心只存储应用名到实例的映射,大幅降低注册中心压力。元数据通过 MetadataService 在 Consumer 和 Provider 间直接同步。

线程模型:默认 All 模式(业务线程和 IO 线程共用),高并发场景建议配置 dispatcher=message 将请求派发到业务线程池,避免 IO 线程阻塞。Provider 端业务线程池默认 FixedThreadPool(200 线程),可配置为 EagerThreadPool 或 LimitedThreadPool。

调用链路:Consumer Proxy → ClusterInvoker(容错策略)→ Directory(服务列表)→ LoadBalance(选实例)→ FilterChain(过滤器,如上下文传递、日志、限流)→ ProtocolInvoker → Netty Client → Netty Server → ProtocolExporter → FilterChain → 实现类。

自适应扩展(Adaptive):运行时根据 URL 参数动态选择扩展实现,如 ProxyFactory$Adaptive 根据 proxy=jdk/javassist 决定使用哪种代理工厂。

工程实践

Dubbo 3.x 推荐 Triple 协议(基于 HTTP/2 + gRPC),兼容跨语言调用。服务分组(group)和版本(version)可用于灰度发布。上下文传递通过 RpcContext 和 Attachment 实现,注意 Attachment 大小限制(默认 8KB)。生产环境建议开启 QoS(默认 22222 端口)用于运维查询。

高频追问
  • Q: Dubbo 泛化调用是什么?A: 无需服务端接口类,通过 GenericService 直接按方法名和参数类型调用。适合网关、测试平台、动态脚本等场景,但性能略低于普通调用。
  • Q: 服务治理有哪些常用手段?A: 动态限流(Sentinel/TpsLimitFilter)、动态熔断、灰度路由(ConditionRouter/TagRouter)、同机房优先(ZoneAwareCluster)、权重动态调整。
Dubbo RPC SPI 负载均衡
E07.07
Sentinel:流量控制、熔断降级、系统自适应保护、热点参数限流高级P1
核心要点
  • 流量控制:基于 QPS、线程数、并发度进行限流,支持直接拒绝、Warm Up(冷启动)、匀速排队(漏桶)三种策略
  • 熔断降级:基于慢调用比例、异常比例、异常数三种策略,熔断后进入 OPEN 状态,经过熔断时长后进入 HALF-OPEN 探测恢复
  • 热点参数限流:对频繁访问的参数值(如用户 ID、商品 ID)进行精细化限流,防止热点数据打垮服务
  • 系统自适应保护:根据系统负载(CPU、负载、RT、线程数、入口 QPS)自动调整流量,防止系统整体过载
  • 滑动窗口统计:默认 1s 分为 2 个 500ms 窗口,实时统计指标;集群限流通过 Token Server 集中分配配额
详细解答

Sentinel 的核心数据结构是 Slot Chain:ProcessorSlotChain 串联一系列插槽(NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot、FlowSlot、DegradeSlot、SystemSlot),每个请求按顺序经过各插槽处理。StatisticSlot 负责统计,FlowSlot 负责限流判断。

限流算法对比:快速失败直接抛异常;Warm Up 适合突发流量,令牌桶初始令牌少,逐步增加到阈值,防止冷系统被压垮;匀速排队严格按固定间隔放行,适合秒杀下游均匀消费,但会阻塞请求。

熔断状态机:CLOSED(正常)→ 达到阈值 → OPEN(拒绝)→ 经过熔断时长 → HALF-OPEN(放行少量请求探测)→ 若成功则 CLOSED,失败则回到 OPEN。HALF-OPEN 状态防止故障恢复后立即涌入全部流量导致二次熔断。

热点参数限流通过 ParamFlowSlot 实现,底层使用 LRU 缓存记录参数频率,对 TopN 参数值单独设限。例如:商品详情页对单个商品 ID 的访问限流 1000 QPS,防止热点商品打爆缓存。

工程实践

Sentinel 规则支持 JSON 配置和 Nacos 动态推送。生产环境建议将规则持久化到 Nacos/Apollo,避免重启丢失。与 Gateway 集成时在网关层做粗粒度限流,业务层做细粒度限流。注意 Sentinel 的 WebFlux 适配器使用方式与 Servlet 不同,需引入 spring-cloud-starter-alibaba-sentinel-gateway。

高频追问
  • Q: Sentinel 与 Hystrix 的区别?A: Sentinel 支持流量控制、热点限流、系统保护,基于滑动窗口实时统计;Hystrix 仅支持熔断降级,基于线程池隔离,自 2018 年起停更、已停止维护(不推荐新项目使用,当前主流替代为 Resilience4j / Sentinel,此处仅作历史对比)。
  • Q: 集群限流如何实现?A: 嵌入 Token Server 模式,各客户端向 Token Server 申请配额;或独立 Token Server 部署,支持 HA 集群。适合多实例共享全局限流阈值。
Sentinel 限流 熔断 流量控制
E07.08
高并发系统架构设计:限流降级、缓存架构、数据库扩展、异步化与无状态化架构P0
核心要点
  • 无状态化:应用层不存储会话/上下文,会话存入 Redis/Token,任意节点可水平扩容,故障可快速迁移
  • 限流降级:接入层(Nginx/Gateway)做粗粒度限流,业务层(Sentinel)做细粒度限流;熔断保护下游,降级保证核心链路可用
  • 缓存架构:多级缓存(CDN → 浏览器 → Nginx 本地缓存 → Redis 分布式缓存 → 本地 Caffeine),缓存穿透用布隆过滤器/空值缓存,缓存击穿用互斥锁/热点预加载,缓存雪崩用随机 TTL/多级缓存
  • 数据库扩展:读写分离(主从复制,查询走从库)、分库分表(ShardingSphere/MyCAT,按用户 ID/时间维度拆分)、NewSQL/TiDB(分布式事务 + 水平扩展)
  • 异步化:MQ 削峰填谷(订单创建后发券/通知异步化)、事件驱动架构(EDA)、CompletableFuture 并行调用聚合、响应式编程(WebFlux/Reactor)
详细解答

高并发设计的核心思想是"分流"和"减负":分流通过负载均衡、CDN、缓存、读写分离将流量分散;减负通过异步化、降级、削峰减少系统实时处理压力。不要试图用单机扛所有流量,水平扩展是无状态服务的基本能力。

缓存一致性方案:Cache-Aside(应用层管理,先更新数据库再删缓存,配合延迟双删)、Read-Through/Write-Through(缓存层管理,如 Caffeine LoadingCache)、Write-Behind(异步写数据库,高性能但数据丢失风险大)。强一致性场景用分布式锁或版本号控制。

数据库扩展的痛点:分库分表后跨分片 JOIN、聚合、排序困难;分布式事务复杂;数据迁移和扩容需停机或双写。TiDB/OceanBase 等 NewSQL 通过分布式 SQL 引擎和 Raft 存储层解决了这些问题,但复杂查询性能可能不及优化后的 MySQL 分片。

全链路压测:生产环境隔离压测流量(通过 Shadow库/影子表或测试标),从网关到数据库全链路验证容量和瓶颈。关键指标:QPS、RT P99、错误率、CPU、内存、GC、连接池饱和度。压测结果驱动容量规划和弹性伸缩策略。

工程实践

架构演进路径:单体 → 垂直拆分 → 服务化(SOA) → 微服务 → 云原生(K8s + Service Mesh)。不要盲目追新,应根据团队规模和业务复杂度选择。中小型团队 Spring Cloud + K8s 足够;超大规模考虑 Service Mesh(Istio)和 Serverless。核心原则:高内聚低耦合、接口契约稳定、可观测性先行。

高频追问
  • Q: 如何设计一个秒杀系统?A: ① 静态资源 CDN;② 验证码/答题削峰;③ 网关限流+黑名单;④ 库存预扣 Redis 原子操作(Lua/Redisson);⑤ 异步 MQ 下单;⑥ 数据库乐观锁/分段锁防止超卖。读多写少场景下库存扣减是核心瓶颈。
  • Q: 微服务拆分过细有什么弊端?A: 分布式事务复杂、网络延迟增加、运维成本上升、接口版本管理困难、测试回归范围扩大。建议按业务域(DDD 限界上下文)拆分,避免过度拆分导致"分布式单体"。
高并发 架构设计 缓存 微服务
E07.09
Seata TCC 模式原理:Try-Confirm-Cancel 三阶段、空回滚/悬挂/幂等治理、与 AT 选型差异架构P1
核心要点

承接 epq-10 的分布式事务对比,本题聚焦 Seata TCC 模式的生产级治理(空回滚/悬挂/幂等)与工程细节。

  • TCC 三阶段:Try(资源预留/冻结)、Confirm(业务确认/扣减冻结)、Cancel(释放冻结/回滚),业务层实现三个接口
  • 与 AT 区别:AT 依赖 Undo Log 自动回滚(无业务侵入);TCC 侵入性强但性能高、可跨异构存储(MySQL + Redis + MQ)
  • 四大经典问题:空回滚(Try 未执行却收到 Cancel)、悬挂(Cancel 先于 Try 到达)、幂等(Confirm/Cancel 重复执行)、并发控制(Try 与 Confirm/Cancel 并发)
  • Seata TCC 实现:@TwoPhaseBusinessAction 注解声明 try/confirm/cancel 方法;TCCResource 注册到 RM;分支事务由 TC 协调
  • 适用场景:跨异构存储、强一致资金类业务、AT 无法覆盖的非关系型存储(Redis 计数器/ES 索引/MQ)
详细解答

Try 阶段:业务层预留资源(如账户余额冻结 100 元,写入 freeze_amount 字段),不直接扣减;Try 失败则整个全局事务回滚。Try 是性能热点,需原子校验(UPDATE account SET balance=balance-100, freeze_amount=freeze_amount+100 WHERE id=? AND balance>=100)。

Confirm 阶段:TC 通知各分支提交,业务层将冻结资源实际扣减(balance-=100, freeze_amount-=100);Confirm 必须成功(失败 TC 自动重试到成功)。Confirm 不再校验业务条件,是纯扣减操作。

Cancel 阶段:TC 通知各分支回滚,业务层释放冻结资源(balance+=100, freeze_amount-=100);Cancel 同样必须成功。

空回滚治理:分支事务表 tcc_branch_log(xid, branch_id, status) 记录状态;Cancel 时若 Try 未执行(无记录),插入"已回滚"标记,阻止后续 Try 写入。悬挂治理:Try 执行前查分支事务表,若已存在 Cancel 记录则直接拒绝 Try(悬挂根因:Cancel 因网络先到,Try 重试后到)。幂等治理:Confirm/Cancel 执行前查状态,已完成则直接返回成功;状态机 Init → Confirmed/Cancelled。

工程实践

表结构设计:业务表预留 freeze 字段(如 account(balance, freeze_amount));额外 tcc_branch_log(xid, branch_id, status, created) 记录分支状态。性能要点:Try 阶段是热点,预留资源需原子操作;Confirm/Cancel 异步化降低 RT。超时配置:全局事务超时需大于业务最长链路 RT;分支事务超时独立配置。异常处理:Confirm/Cancel 失败 TC 自动重试(默认 5 次指数退避);重试耗尽告警人工介入。与 AT 混用:Seata 支持同一全局事务内 AT + TCC 混合分支,但建议同业务域统一模式降低运维复杂度。资金类业务必选 TCC(Undo Log 在异构存储无法回滚)。

高频追问
  • Q: TCC 为什么比 AT 性能好?A: AT 在分支执行阶段需写 Undo Log + 申请全局锁,Confirm/Cancel 阶段异步清理 Undo Log 并释放全局锁,整体 RT 与锁竞争开销高于 TCC 的纯业务 Confirm;TCC Confirm 是纯业务操作(无全局锁、无 Undo Log),且 Try 阶段就完成了资源校验,冲突少、RT 低。
  • Q: TCC 的开发成本如何降低?A: 模板化代码生成(MyBatis Plugin 生成 try/confirm/cancel);Seata 2.x 提供 TCC 注解 + 自动状态管理;封装通用 freeze/release 工具类;同一业务域内抽象 TCC 模板方法。
  • Q: 资金类业务为什么必须 TCC?A: 资金要求强一致 + 跨账户原子性 + 审计可追溯;AT 的 Undo Log 在异构存储(Redis 计数器、ES 索引、MQ)无法回滚;TCC 业务层回滚可覆盖任意存储类型。
  • Q: TCC 与 Saga 怎么选?A: TCC 适合短事务、强一致、资源可预留;Saga 适合长事务、最终一致、业务流程长(如订单全链路);Saga 无 Try 阶段,补偿事务时前置事务已提交,存在数据可见窗口。
Seata TCC 分布式事务 强一致
E07.10
Outbox / CDC 端到端速查:本地事务同库落箱、Debezium 捕获、投递幂等与对账架构P1
核心要点
  • 问题:业务写库与发 MQ 非原子 → 丢消息或重复;Outbox 把「事件」写入同库同事务
  • 链路:业务表 + outbox 同事务提交 → CDC(Debezium/Canal)读 binlog/redo → MQ → 消费者幂等
  • 与 2PC/Seata:Outbox 是最终一致;强一致短事务仍看 AT/TCC(E07.05/09)
  • 幂等:业务键 / Idempotency-Key;Outbox 去重靠主键或 (aggregate_id, event_id)
  • 真源互补:深挖见 C09.16;微服务一致性见 C10.11
详细解答

为何 Outbox:先发 MQ 再写库可能「消息有、库无」;先写库再发 MQ 可能「库有、消息无」。同事务写入 outbox 表,把可靠投递交给 CDC/轮询,业务代码只保证本地 ACID。

CDC vs 轮询:轮询简单但延迟与负载高;Debezium 读日志近实时、对业务表无侵入。Canal 偏 MySQL 生态;跨库/PG 优先 Debezium。

至少一次语义:CDC/MQ 默认至少一次 → 消费者必须幂等;Outbox 行可用 processed 标记或删行,防重复投递风暴。

对账:业务表计数 vs outbox 已投递 vs MQ 消费水位;缺口告警 + 补投工具。勿假设「CDC 永远不丢」。

工程实践

outbox 字段建议:id、aggregate_type/id、event_type、payload、created_at、status。大 payload 可外置对象存储只存指针。分区/归档按时间切表。与 Saga 编排:Outbox 发「领域事件」,编排器消费推进状态机。

高频追问
  • Q: Outbox 和本地消息表是一回事吗?A: 同源思想。本地消息表常靠应用轮询;现代多指同库落箱 + CDC 外发。
  • Q: 为何不直接双写 MQ?A: 跨资源无共享事务;双写必有窗口期不一致。
  • Q: CDC 延迟抖动怎么治?A: 监控 replication lag;热点大事务拆小;Connector 并行度与 topic 分区对齐。
Outbox CDC Debezium 最终一致

八、消息队列

本组知识链

本组沿消息系统能力梯:Kafka 高性能机制 → RocketMQ 业务语义 → 消费者组与 Exactly-Once → 流处理状态与水位线。

  1. Kafka 性能:顺序写、页缓存、零拷贝、批量压缩、分区并发。
  2. RocketMQ 语义:事务/延迟/顺序消息与 Broker 复制。
  3. 消费语义:重平衡、位移提交、Exactly-Once 边界。
  4. 流处理:Kafka Streams vs Flink、状态后端、Watermark、端到端 EOS。
工程化要点
  • 主题分区与消费者并发按积压 SLO 设计,重平衡有演练。
  • 位移提交策略与幂等消费成对出现。
  • 事务消息/本地消息表选型写进一致性评审。
  • 积压告警与扩容/降级 Runbook 绑定。
  • 顺序消息的分区键与业务顺序假设显式化。
  • 流处理状态后端备份与恢复进演练。
  • 跨机房复制的 RPO/RTO 写进容灾目标。
本组思维模式
  • 吞吐来自分区:热点分区比「再加 Broker」更常见。
  • 至少一次是默认:业务幂等是 Exactly-Once 的落地。
  • 语义先于性能:先定顺序/事务/延迟需求再选产品能力。
  • 状态有寿命:流处理状态与水位线决定正确性。
  • 积压是一等故障:按积压分钟数而不是 QPS 好看度值班。
E08.01
Kafka 高性能原理:顺序写、页缓存、零拷贝、批量压缩、分区并发架构P0
核心要点
  • 顺序写:Kafka 日志文件追加写入,磁盘顺序写性能接近内存(机械磁盘也能达数百 MB/s)
  • 页缓存(PageCache):消息先写入 OS 页缓存,由操作系统异步刷盘,消费时直接从页缓存读,不经过磁盘
  • 零拷贝:Kafka 使用 FileChannel.transferTo 实现 sendfile,消息从页缓存直接发送到网卡,不经过用户态
  • 批量压缩:Producer 批量发送(batch.size/linger.ms),支持 GZIP/Snappy/LZ4/ZSTD 压缩,降低网络带宽
  • 分区并发:Topic 分为多个 Partition,每个 Partition 独立读写,消费者组内消费者并行消费不同 Partition
详细解答

Kafka 将每个 Partition 映射为一个日志文件(.log)和索引文件(.index/.timeindex)。Producer 发送消息时,Broker 将消息追加到日志文件末尾,这是顺序写操作。相比随机写,顺序写避免了磁头寻道,磁盘吞吐量可提升 1~2 个数量级。

页缓存机制让 Kafka 的读写都可能在内存中完成:Producer 写入时数据先到页缓存,Consumer 读取时若数据仍在页缓存则直接内存读取。只有当页缓存不足或显式刷盘时才会写磁盘。这也解释了为什么 Kafka 的吞吐与内存大小密切相关。

零拷贝的应用场景是 Consumer 拉取消息:传统方式需 4 次拷贝(磁盘→内核→用户→内核 socket→网卡),Kafka 通过 FileChannel.transferTo 让数据直接从页缓存发送到网卡,只需 2 次 DMA 拷贝且零 CPU 参与。

批量压缩在 Producer 端完成,将多条消息压缩成一个 RecordBatch 发送。压缩算法选择:Snappy/LZ4 追求速度,ZSTD 追求压缩比(Kafka 2.1+ 支持)。压缩后网络传输量和磁盘占用大幅降低。

分区是 Kafka 并发的基础,但分区数不是越多越好:每个分区对应一个日志目录下的文件集合,分区过多会导致文件句柄耗尽、页缓存分散、消费重平衡时间增加。一般建议单 Broker 不超过 4000 分区,单集群不超过 20 万分区。

工程实践

Producer 调优:batch.size=32KB~128KB,linger.ms=5~20ms,compression.type=lz4。Broker 调优:log.flush.interval.messages=10000(异步刷盘),num.network.threads 和 num.io.threads 根据 CPU 核数调整。

高频追问
  • Q: Kafka 为什么不像 RabbitMQ 那样支持优先级队列?A: Kafka 的设计哲学是顺序日志,优先级队列会破坏分区内顺序性。若需优先级,可用多个 Topic 分别对应不同优先级,消费者优先消费高优 Topic。
  • Q: Kafka 的 Exactly-Once 如何实现?A: Producer 幂等(PID + Sequence Number)+ 事务(Transaction Coordinator)+ Consumer 手动提交 offset 且处理逻辑幂等。三者配合实现端到端 Exactly-Once。
Kafka 零拷贝 批量压缩 页缓存
E08.02
RocketMQ:事务消息、延迟消息、顺序消息、Broker 主从复制架构P1
核心要点
  • 事务消息:基于半消息(Half Message)+ 事务回查实现,确保本地事务与消息发送最终一致,解决生产端的分布式事务问题
  • 延迟消息:Broker 内置 18 个延迟级别(1s~2h),消息到期后从 SCHEDULE_TOPIC_XXXX 转发到目标 Topic;5.0 支持任意秒级延迟
  • 顺序消息:同一队列内保证 FIFO,全局顺序需单 Topic 单队列,分区顺序按 Sharding Key 哈希到固定队列
  • 主从复制:同步双写(SYNC_MASTER)保证数据不丢但吞吐低;异步复制(ASYNC_MASTER)吞吐高但故障可能丢消息
  • 存储架构:CommitLog 顺序写 + ConsumeQueue 稀疏索引,所有 Topic 共享一个 CommitLog,充分利用磁盘顺序写性能
详细解答

事务消息流程:① Producer 发送 Half Message 到 Broker;② 执行本地事务;③ 根据本地事务结果发送 Commit 或 Rollback;④ Broker 若未收到确认,定时回查 Producer 事务状态。Half Message 对消费者不可见,Commit 后才可消费。

延迟消息实现:Broker 将延迟消息按延迟级别投递到对应的 SCHEDULE_TOPIC 队列,定时任务扫描到期消息后重新投递到真实 Topic。RocketMQ 5.0 引入 TimerWheel 支持任意延迟,精度到秒级。

顺序消息的限制:若某队列对应的 Broker 宕机,该队列消息消费暂停,需等待主从切换。消费端失败时默认暂停当前队列消费(而非跳过),防止乱序。顺序消息消费并行度等于队列数。

Broker 高可用:4.5 前需部署 SYNC_MASTER + SLAVE,主宕后需人工切换;4.5+ 引入 Dledger 自动选主,基于 Raft 协议实现自动故障转移。

工程实践

事务消息适合订单创建后发券、扣库存后发通知等场景。延迟消息适合超时关单、延迟对账。生产环境推荐 SYNC_MASTER + Dledger 部署,消息刷盘策略选 SYNC_FLUSH(金融场景)或 ASYNC_FLUSH(普通场景)。消费幂等需业务方通过唯一键保证。

高频追问
  • Q: RocketMQ 与 Kafka 的选型?A: RocketMQ 延迟消息、事务消息、消息轨迹更成熟,适合电商金融;Kafka 吞吐更高、生态更广,适合大数据日志采集、流处理。
  • Q: 消息重复消费如何处理?A: RocketMQ 保证至少一次投递,业务消费端必须幂等。可通过数据库唯一索引、Redis setnx、状态机校验等方式实现。
RocketMQ 事务消息 延迟消息 Broker
E08.03
Kafka 消费者组:重平衡机制、分区分配策略、位移提交、Exactly-Once架构P1
核心要点
  • 消费者组(Consumer Group):组内消费者共同消费一个 Topic 的所有分区,每条消息仅被组内一个消费者处理;不同组独立消费全量数据
  • 重平衡(Rebalance):消费者加入/退出/崩溃或分区数变化时,Coordinator 重新分配分区所有权。触发期间整个组暂停消费
  • 分区分配策略:Range(按 Topic 范围分配,可能不均衡)、RoundRobin(轮询,较均衡)、Sticky(尽量保持原有分配,减少迁移)、CooperativeSticky(增量重平衡,Kafka 2.4+ 默认)
  • 位移提交:自动提交(默认 5s,可能重复/丢失)、手动同步提交(阻塞但精确)、手动异步提交(性能高但需处理异常)
  • Exactly-Once:生产者开启幂等(enable.idempotence)+ 事务(transactional.id)+ 消费者关闭自动提交并在事务中提交 offset
详细解答

重平衡流程(Eager 协议):① Coordinator 通知所有消费者停止消费并撤销分区;② 消费者重新加入组发送 JoinGroup 请求;③ Coordinator 选 Leader 消费者,Leader 执行分区分配;④ Coordinator 下发分配结果,消费者开始消费。此过程全组不可用。

Cooperative Rebalance(增量重平衡):消费者先撤销将被迁移的分区,保留不变的分区继续消费,然后只重新分配变更的部分。大幅减少了重平衡期间的停顿时间,Kafka 2.4+ 推荐。

位移提交的语义:At-most-once(先提交后处理,可能丢失数据)、At-least-once(先处理后提交,可能重复消费)、Exactly-once(事务保证提交与处理原子性)。幂等生产者通过 PID + Sequence Number 去重,确保单分区有序且不重复。

消费者协调器(Coordinator):每个 Broker 都可能成为某消费者组的 Coordinator,组名哈希到 __consumer_offsets 主题的特定分区,该分区的 Leader Broker 即为 Coordinator。

工程实践

避免不必要的重平衡:合理设置 session.timeout.ms(默认 45s)和 heartbeat.interval.ms(默认 3s),避免 GC 或网络抖动导致误判下线。消费者数不要超过分区数,多余的消费者空闲。分区数调整需权衡并行度和重平衡开销。

高频追问
  • Q: 消费者提交 offset 失败后如何恢复?A: 若使用手动提交,捕获 CommitFailedException 后可重试或记录到死信。自动提交失败可能导致重复消费,业务需保证幂等。
  • Q: 消费者数大于分区数会怎样?A: 多余的消费者无法分配到分区,处于空闲状态。若其中某个消费者崩溃不会触发重平衡。建议消费者数等于或略小于分区数。
Kafka 消费者组 重平衡 Exactly-Once
E08.04
流处理速查:Kafka Streams vs Flink、状态后端、Watermark 与端到端 Exactly-Once架构P1
核心要点
  • 定位:Kafka Streams = 轻量库内嵌应用;Flink = 独立集群、复杂窗口/CEP/大状态
  • 状态:本地 RocksDB + changelog topic(Streams)/ Checkpoint+Savepoint(Flink)
  • 时间:事件时间 + Watermark;乱序靠允许延迟,迟到数据侧输出或丢弃策略
  • EOS:Kafka 事务 / Flink two-phase commit sink;端到端需源+算子+汇全链路配合
  • 深挖:C11.24 / C11.25;CEP 场景 S11.03
详细解答

选型一句:同 Kafka 生态、状态可控、运维想省 → Streams;要窗口复杂、反压成熟、Savepoint 运维、多源汇 → Flink。

Watermark:声明「该时间之前的数据基本到齐」。水位推进太激进丢迟到;太保守窗口关不掉、状态膨胀。

Exactly-Once 边界:框架 EOS ≠ 业务幂等。外部副作用(HTTP/邮件)仍要业务键去重。Kafka→Kafka 路径 EOS 最完整。

背压:Flink 信用反压;Streams 主要靠消费线程阻塞。生产必看 checkpoint 时长与 state 大小。

Kafka Source ↔ Flink 对照速表:① 分区有序对 Flink keyBy 友好,乱序靠 Watermark;② Broker 事务超时须覆盖 Checkpoint;③ changelog(Streams)≈ Checkpoint 存储(Flink),都要算容量;④ 换并行度:Streams 受分区数约束,Flink 走 Savepoint。

工程实践

状态 TTL 与窗口对齐,防无限增长。Changelog/Checkpoint 存储与 Kafka 同可用区。变更拓扑用版本化应用 ID / Savepoint 兼容策略。指标:消费 lag、checkpoint duration、numLateRecords。对接 Kafka 时固定作业 UID,禁止无评估重置 offset。

高频追问
  • Q: 为什么处理时间不够?A: 乱序与延迟下窗口结果不稳定;事件时间+Watermark 才可复现。
  • Q: Streams 能替代 Flink 吗?A: 简单聚合/连接可以;复杂 CEP、超大状态、多引擎编排通常不行。
  • Q: EOS 失败最常见原因?A: sink 非事务/非幂等、跨系统双写、checkpoint 超时导致重复。
Kafka Streams Flink Watermark Exactly-Once

九、网络 IO 与安全

本组知识链

本组从传输性能到身份安全:Netty Reactor 与零拷贝 → TLS1.3 → OAuth2.1/PKCE → HTTP/2/QUIC 选型 → JWT 安全边界。

  1. 高性能 IO:Reactor、sendfile/mmap/DirectBuffer 与 Netty 落点。
  2. 传输安全:TLS1.3 握手、证书链、前向安全。
  3. 授权演进:授权码+PKCE,废弃隐式/密码模式,Refresh 轮换。
  4. 协议与令牌:多路复用与队头阻塞;JWT 签名、声明校验、撤销。
工程化要点
  • 证书到期告警绑定轮换 Runbook,禁止只告警不演练。
  • OAuth 客户端必须 PKCE;密钥/客户端密钥轮换有双写窗口。
  • JWT 算法白名单与密钥轮换/撤销列表进门禁。
  • 长连接与连接池参数纳入容量模型。
  • HTTP/2、gRPC、QUIC 选型对照队头阻塞与运维成熟度。
  • DirectBuffer/堆外泄漏有专项检测。
  • TLS 套件与协议版本基线定期审计。
本组思维模式
  • 零拷贝有边界:文件路径与堆外生命周期要一起设计。
  • 身份是供应链:令牌与证书轮换失败=全面事故。
  • 协议选型看阻塞:多路复用不自动等于低延迟。
  • 默认安全:废弃模式不得因「省事」回流。
  • 声明校验完整:只验签名不够,还要验 aud/iss/exp 与撤销。
E09.01
Netty 高性能:Reactor 线程模型、零拷贝(sendfile/mmap/DirectBuffer)架构P1
核心要点
  • Reactor 模型:单线程 Reactor(一个 Selector 处理所有 IO)→ 多线程 Reactor(主从 Selector 分离)→ Netty 主从多线程模型
  • Netty 线程模型:BossGroup(1 个线程)接受连接,WorkerGroup(N 个线程)处理读写,业务线程池可选
  • 零拷贝:FileRegion.transferTo 调用 sendfile,文件数据直接从内核缓冲区发送到网卡,不经过用户态
  • CompositeByteBuf 将多个 ByteBuf 逻辑合并,避免内存拷贝;DirectBuffer 使用堆外内存,减少 GC 压力
  • 对象池 Recycler 复用 ByteBuf 等对象,减少 new 和 GC;FastThreadLocal 替代 ThreadLocal,避免 Hash 冲突
详细解答

Netty 的 主从 Reactor 模型 中,Boss NioEventLoop 负责 Accept 新连接,每接受一个连接就注册到 Worker NioEventLoop 的 Selector 上。Worker 处理读写事件后,将解码后的消息交给 ChannelPipeline 中的 Handler 链处理。若业务耗时,应提交到独立的业务线程池,避免阻塞 Worker 线程。

零拷贝的三种实现:

  • sendfile:传统文件发送需 4 次拷贝(磁盘→内核→用户→内核 socket→网卡),sendfile 通过 DMA 直接从内核页缓存到网卡,仅需 2 次拷贝且无需用户态参与
  • mmap:文件映射到用户态虚拟地址空间,读写直接操作内存,减少 read/write 系统调用
  • DirectBuffer:JVM 堆外内存,不受 GC 影响,可直接用于 NIO 通道读写,避免 HeapBuffer 的额外拷贝(堆内存→直接内存)

Netty 的 ByteBuf 相比 JDK ByteBuffer 的优势:支持动态扩容、读写索引分离(readerIndex/writerIndex)、引用计数( retain()/release())实现池化复用。UnpooledHeapByteBuf 使用堆内存,UnpooledDirectByteBuf 使用直接内存,PooledByteBuf 从对象池分配。

工程实践

文件传输场景使用 DefaultFileRegion 配合 ChannelHandlerContext.writeAndFlush() 触发 sendfile。高并发下开启 -Dio.netty.leakDetectionLevel=advanced 检测 ByteBuf 内存泄漏,生产环境建议用 PooledByteBufAllocator。

高频追问
  • Q: Netty 的 EventLoop 为什么绑定一个线程?A: 单线程处理单个 Channel 的所有 IO 事件,避免多线程切换和锁竞争,同时保证事件处理顺序性。但一个 EventLoop 可以注册多个 Channel,通过 Selector 多路复用。
  • Q: 为什么 Netty 不用 JDK 的 ThreadPoolExecutor?A: ThreadPoolExecutor 的阻塞队列在任务大量提交时可能 OOM;Netty 的 EventLoop 使用 MpscQueue(多生产者单消费者无锁队列),更适合 IO 场景的高吞吐。
Netty NIO 零拷贝 Reactor
E09.02
HTTPS/TLS 1.3 握手:证书链验证、密钥交换、前向安全高级P0
核心要点
  • TLS 1.3 握手仅需 1-RTT(相比 TLS 1.2 的 2-RTT),若使用 PSK(Pre-Shared Key)可实现 0-RTT
  • 密钥交换:ECDHE(临时椭圆曲线 Diffie-Hellman)生成临时密钥对,每次会话密钥不同
  • 前向安全(Forward Secrecy):即使服务器私钥泄露,历史会话也无法解密,因为会话密钥由临时 ECDHE 生成
  • 证书链验证:客户端从服务器证书逐级验证到受信根 CA,检查有效期、域名、证书吊销状态(OCSP)
  • TLS 1.3 废弃了 RSA 密钥交换(无前向安全)和大量旧密码套件(如 CBC、MD5、SHA-1),仅保留 AEAD 算法
详细解答

TLS 1.3 握手流程(1-RTT):

  1. Client Hello:客户端发送支持的密码套件列表、key_share(ECDHE 公钥)、随机数
  2. Server Hello + EncryptedExtensions + Certificate + CertificateVerify + Finished:服务端回复选择的密码套件、自己的 key_share、证书链、证书签名验证、Finished 消息
  3. 客户端验证证书链和签名后,用双方 key_share 计算出会话密钥,发送 Finished

相比 TLS 1.2,1.3 将 ServerKeyExchange 和 CertificateRequest 等消息合并到加密扩展中,且密钥交换与身份验证分离,实现 1-RTT。

前向安全的本质:ECDHE 生成的临时密钥对在握手后即丢弃,会话密钥由双方临时公钥派生。即使攻击者记录所有通信流量并在未来获取服务器私钥,也无法计算出历史会话密钥。

证书链验证过程:服务器证书 → 中间 CA 证书 → 根 CA 证书。客户端检查每个证书的有效期、域名匹配(Subject Alternative Name)、密钥用途扩展,并用上级证书的公钥验证下级签名。根证书预置在操作系统信任库中。

工程实践

服务器配置 TLS 1.3 时禁用 TLS 1.0/1.1,密码套件仅保留 TLS_AES_256_GCM_SHA384 等 AEAD 组合。证书使用 ECDSA 而非 RSA,握手更快。开启 OCSP Stapling 减少客户端证书验证延迟。

高频追问
  • Q: TLS 1.3 的 0-RTT 有什么安全隐患?A: 0-RTT 使用 PSK 恢复会话,但数据在握手完成前已发送,缺乏前向安全保证,且可能遭受重放攻击。敏感操作(如支付)不应依赖 0-RTT。
  • Q: 为什么 TLS 1.3 废弃了 RSA 密钥交换?A: RSA 密钥交换不支持前向安全(会话密钥由服务器私钥加密传输,私钥泄露可解密历史会话)。TLS 1.3 强制使用 ECDHE 保证前向安全。
HTTPS TLS 1.3 证书链 前向安全
E09.03
OAuth 2.1 + PKCE:授权码+PKCE 流程、为什么废弃隐式/密码模式、Refresh Token 轮换与安全增强高级P2
核心要点
  • OAuth 2.1 整合:合并 RFC 6749(OAuth 2.0)+ 多个最佳实践 RFC(7662 Introspection、7009 Revocation、6819 Threat Model),单一规范
  • 强制 PKCE:所有客户端(含机密客户端)必须使用 PKCE(Proof Key for Code Exchange),防御授权码拦截攻击
  • 废弃流程:Implicit Grant(隐式授权,token 直接返回前端)+ Password Grant(资源所有者密码凭证,密码泄漏风险)
  • 推荐流程:Authorization Code + PKCE(前端/SPA/移动端)、Client Credentials(服务间)、Device Code(IoT/无输入设备)
  • 安全增强:Refresh Token 轮换 + 检测重用、精确 redirect_uri 匹配、禁止 http(除 localhost)、强制 TLS 1.2+
详细解答

Authorization Code + PKCE 流程:① 客户端生成 code_verifier(随机 43~128 字符)+ code_challenge = BASE64URL(SHA256(verifier)) ② 携带 code_challenge 发起授权请求 ③ 用户授权,授权服务器返回 authorization_code ④ 客户端用 code + code_verifier 换 access_token ⑤ 服务器校验 SHA256(verifier) == challenge,通过则发 token。

为什么废弃 Implicit:Implicit 直接在 redirect_uri fragment 返回 access_token,① token 暴露在浏览器历史/Referer/扩展 ② 无 refresh token ③ 无法做 Client Authentication ④ 易受 redirect_uri 劫持;PKCE + Authorization Code 解决了所有这些问题(code 换 token 一步在后台完成,token 不经过浏览器)。

为什么废弃 Password:Password 模式让客户端接触用户密码,违反 OAuth"令牌隔离"原则;密码强度/2FA/SSO 都无法在 Password 模式下实现;开放生态下(第三方 App)密码泄漏风险极高。规范改为:自有客户端用 Authorization Code + PKCE,第三方一律不接触密码。

PKCE 防御原理:攻击者拦截 authorization_code(如通过恶意 app 注册的 custom URI scheme),但无 code_verifier 无法换 token;code_verifier 仅客户端内存持有,不通过网络传输。Refresh Token 轮换:每次刷新发新 refresh_token + 旧 token 失效;若被劫持的旧 token 再次使用,服务器检测到"重用"立即撤销整条 token 链(Token Rotation + Reuse Detection)。

工程实践

Spring Authorization Server:1.1+ 完整支持 OAuth 2.1;OAuth2AuthorizationServerConfiguration 开启授权服务器,RegisteredClient 配置客户端 + PKCE 强制。前端/SPA 集成:用 @azure/msal-browser / AppAuth / oauth4web 等成熟库,不要手撸 PKCE;code_verifier 用 Web Crypto API(crypto.getRandomValues)。Token 存储策略:access_token 内存(不存 localStorage 防 XSS);refresh_token HttpOnly + Secure + SameSite=Strict Cookie;ID Token 由前端校验签名。生产配置:redirect_uri 精确匹配(禁止通配符)、access_token 短生命周期(5~15 分钟)、refresh_token 长生命周期(7~30 天)+ 轮换。监控告警:PKCE 校验失败率、refresh token 重用事件、异常 redirect_uri 请求;接入 WAF 防止 token 端点暴力枚举。

高频追问
  • Q: PKCE 也能被中间人攻击吗?A: PKCE 防的是 authorization_code 拦截(不含 verifier 无法换 token);若 TLS 被破,全链路都危。PKCE 必须配合 HTTPS 才有意义,不替代 TLS。
  • Q: 后端服务间调用还用 PKCE 吗?A: 不用。服务间用 Client Credentials Grant,客户端密钥(client_secret)做认证;PKCE 针对的是"无法安全存储密钥"的公开客户端(SPA/APP)。
  • Q: OAuth 2.1 与 OIDC 是什么关系?A: OAuth 2.1 是授权框架(authorization,决定"能做什么"),OIDC 是认证协议(authentication,决定"是谁")建立在 OAuth 2.1 之上;OIDC 在 OAuth 2.1 流程上加 ID Token + UserInfo 端点。
  • Q: 为什么不让所有客户端都用 client_secret?A: SPA/移动端无法安全存储 secret(反编译/前端可见),PKCE 用动态 verifier 替代静态 secret,"一次一密"更安全;机密客户端(后端服务)仍用 client_secret + PKCE 双重保护。
OAuth 2.1 PKCE 安全 认证授权
E09.04
TCP / HTTP/2 / QUIC:多路复用、队头阻塞与选型边界架构P1
核心要点
  • TCP:可靠字节流;队头阻塞在传输层丢包时拖住整连接
  • HTTP/2:单 TCP 上多路流,应用层多路;仍受 TCP 队头阻塞
  • HTTP/3/QUIC:UDP 上多路 + 按流独立丢失恢复,改善弱网
  • 工程:网关/客户端超时、流控窗口、连接复用与证书
  • 深挖:C13 网络篇;TLS 见 E09.02
详细解答

选型表达:内网高带宽稳定 → HTTP/1.1/2 足够;移动/弱网长连接 → 评估 HTTP/3。gRPC 默认 HTTP/2,关注流控与连接数。

队头阻塞要分层说:HTTP/1.1 是应用层;HTTP/2 缓解应用层但仍受 TCP;QUIC 把丢失恢复下沉到流级。

工程实践

网关记录协议版本占比;调 window 与最大并发流;证书与 ALPN 配置进发布检查。

高频追问
  • Q: HTTP/2 是否消灭队头阻塞?A: 消灭应用层队头,传输层 TCP 丢包仍阻塞整连接。
  • Q: 为何 gRPC 爱 HTTP/2?A: 多路流 + 二进制帧 + 头部压缩,适合 RPC。
HTTP/2 QUIC TCP
E09.05
零拷贝深化:sendfile、mmap、DirectBuffer 与 Netty 落点高级P1
核心要点
  • 传统 read/write:多次用户态/内核态拷贝
  • sendfile:文件→socket 路径减少拷贝(FileRegion)
  • mmap:文件映射,适合随机访问,不替代所有发送路径
  • DirectBuffer:堆外,避免堆→直接内存二次拷贝,需防泄漏
  • 深挖:E09.01;泄漏检测 io.netty.leakDetectionLevel
详细解答

「零拷贝」是相对术语:DMA/页缓存仍可能拷贝,工程目标是减少用户态拷贝与系统调用。静态文件下载优先 sendfile;协议编解码缓冲优先池化 DirectBuffer。

工程实践

大文件下载用 FileRegion;开启泄漏检测 staging;生产用 PooledByteBufAllocator。监控直接内存与堆内存分列。

高频追问
  • Q: DirectBuffer 会不会 OOM?A: 会;受 MaxDirectMemorySize 约束,泄漏更难查。
  • Q: mmap 一定更快吗?A: 小文件/短连接不一定;要测页错误与回收成本。
零拷贝 sendfile DirectBuffer
E09.06
JWT 安全速查:签名算法、声明校验、密钥轮换与撤销高级P2
核心要点
  • 结构:Header.Payload.Signature;默认仅签名(JWS),敏感载荷用 JWE
  • 必验:iss/aud/exp/nbf、算法白名单(禁 alg=none)
  • 密钥:非对称优先(RS/ES);轮换用 kid;泄露即撤销
  • 会话:短 access + 刷新轮换;服务端黑名单/版本号做紧急吊销
  • 深挖:E09.03 OAuth;C11 JWT/OAuth 篇章
详细解答

JWT 是令牌格式不是会话方案。无状态带来难撤销:靠短 TTL、Refresh 轮换检测重用、或版本号/黑名单。

算法混淆攻击:校验时固定期望算法,不信任 header 自报。

工程实践

网关统一验签;密钥进 KMS;kid 轮换演练;监控验签失败率与 refresh 重用事件。

高频追问
  • Q: 为什么不把权限全塞 JWT?A: 难撤销且体积大;权限变更延迟。关键权限宜实时查询或短 TTL。
  • Q: 对称 HS256 何时可用?A: 单服务内密传可;多服务分发密钥难,优先非对称。
JWT 密钥轮换 安全

十、搜索与 AI

本组知识链

本组连接经典检索与向量检索:倒排与 BM25 → ES 分片副本段合并 → HNSW/IVF 取舍 → Mapping 治理 → RAG 双写同步与评测。

  1. 倒排检索:分词、postings、Skip List、BM25 相关性。
  2. 集群工程:分片路由、副本、段合并、写入与搜索调优。
  3. 向量索引:HNSW vs IVF 的召回/内存/构建成本。
  4. RAG 同步:Mapping 防字段爆炸;双写一致性、重建切换与评测门禁。
工程化要点
  • 索引 Mapping 变更走评审,动态模板防字段爆炸。
  • 分片数与副本策略按数据增长预留,避免事后拆分灾难。
  • 相关性调优有离线评测集,禁止只凭感觉加权重。
  • 向量索引选型表(召回/QPS/内存)强制入库。
  • RAG 索引切换必须双跑与回滚开关。
  • 重建与全量导入有窗口与限速,保护在线集群。
  • 检索与生成链路的延迟预算分段可观测。
本组思维模式
  • 相关性可评测:没有黄金集的调参是赌博。
  • 结构先于算法:Mapping/分片错误比打分公式更致命。
  • 向量有成本面:召回率与内存/构建时间三角。
  • 同步是变更:索引切换与代码发布同级。
  • 双跑证明:切换前用影子流量证明不回归。
E10.01
Elasticsearch 倒排索引:分词、postings list、Skip List、BM25 打分高级P1
核心要点
  • 倒排索引:词项(Term)→ 文档 ID 列表(postings list),与正排索引(文档→词项)相反
  • 分词:Analyzer 将文本拆分为 Token,经字符过滤→分词→Token 过滤(小写、去停用词、同义词)
  • postings list 存储文档 ID、词频(TF)、位置(Position)、偏移量(Offset)
  • Skip List:对 postings list 分层索引,加速合并操作(如 AND/OR 查询),将 O(n) 降为 O(log n)
  • BM25:基于概率的排序算法,考虑词频(TF)、逆文档频率(IDF)、字段长度归一化,比 TF-IDF 更鲁棒
详细解答

ES 的索引过程:文档写入时,每个字段经 Analyzer 分词生成 Token Stream,每个 Token 记录所在文档 ID、词频、位置。这些 Token 按字典序排序后构建倒排索引。

postings list 的存储采用 FOR(Frame of Reference)+ RBM(Roaring Bitmap)压缩:对有序文档 ID 列表,先按块(如 128 个)分块,块内用差值编码(Delta Encoding),再用位图存储。这样既节省空间,又支持快速的集合交并运算。

Skip List 在 postings list 上建立多级索引:每 128 个文档 ID 建一个 skip point,记录该点的文档 ID 和在列表中的位置。查询时先比较 skip point,快速跳过不匹配的块。例如查询"包含 term A 和 term B 的文档",合并两个 postings list 时,用 Skip List 快速定位到共同文档 ID,避免逐一遍历。

BM25 公式核心:

score(D,Q) = Σ IDF(qi) · [ TF(qi,D) · (k1+1) ] / [ TF(qi,D) + k1 · (1-b + b·|D|/avgdl) ]

其中 k1 控制词频饱和度(默认 1.2),b 控制字段长度惩罚(默认 0.75)。BM25 相比 TF-IDF 的优势:词频不再线性增长,而是趋于饱和;字段长度归一化更合理,避免长文档因词频高而排名靠前。

工程实践

中文分词推荐 IK Analyzer 或 jieba,注意同义词扩展和停用词过滤。对于日志场景,使用 keyword 类型做精确匹配和聚合,text 类型做全文搜索。调优时关注 index.refresh_interval(默认 1s,批量写入可调大到 30s)。

高频追问
  • Q: ES 的索引为什么不实时可见?A: 文档写入先存于内存缓冲区(refresh 前不可见),默认每秒 refresh 一次生成新段(segment)。近实时(NRT)而非实时。force refresh 性能开销大。
  • Q: 为什么 ES 不适合做 primary 数据库?A: ES 更新是标记删除 + 新写段,非原地更新;事务支持弱;复杂关联查询性能差。适合搜索和分析,不适合强一致性事务场景。
ES 倒排索引 BM25 分词
E10.02
Elasticsearch:分片路由、副本同步、段合并、搜索与写入调优架构P2
核心要点
  • 分片路由:文档通过 shard = hash(routing) % 主分片数 路由,默认 routing 为 _id,自定义 routing 可将相关文档路由到同一分片减少查询扇出
  • 副本同步:主分片(Primary)接收写请求,并行复制到所有副本(Replica),多数副本确认后返回成功(wait_for_active_shards)
  • 近实时搜索:写入数据先进入内存缓冲区(In-Memory Buffer)和 Translog,每秒 refresh 生成新 Segment(此时可搜),30min 或 Translog 过大时 flush 落盘
  • 段合并(Merge):后台将小的 Segment 合并为大的 Segment,减少文件句柄和查询时的扇出;合并期间旧 Segment 仍可搜,合并完成后原子替换
  • 写入调优: bulk 批量写入、增大 refresh_interval(如 30s)、关闭不必要的副本、使用 auto_generate_id 避免版本检查
详细解答

分片数量一旦创建不可更改(副本数可调)。主分片数决定了索引的最大数据分布粒度,过多分片导致集群元数据膨胀、查询扇出过大;过少则无法利用多节点并行。建议单分片大小 20~50GB,避免超过 100GB。

写入流程:Coordinator 节点根据 routing 转发到主分片 → 主分片写内存 buffer 和 translog → 主分片并行转发到副本 → 副本写 buffer/translog 并确认 → 主分片返回成功。refresh 操作将 buffer 中的数据生成 Segment 并清空 buffer,此时数据对搜索可见,但尚未 fsync 到磁盘。

段合并策略:Tiered Merge Policy 按段大小分层合并,优先合并相近大小的段。force_merge 可手动触发,但会消耗大量 IO 和 CPU,建议在低峰期执行。被删除的文档仅在段合并时物理清除。

脑裂防护:7.x 前通过 discovery.zen.minimum_master_nodes 设置;7.x 后基于 Raft 的选举机制,要求多数节点同意。主节点负责集群状态管理,数据节点负责分片读写,应分离部署。

工程实践

映射设计避免频繁刷盘:对只读历史索引执行 force_merge 到 1 个 Segment 并设置 index.codec: best_compression。分片分配感知机架(awareness.attributes: rack_id)防止同一机架故障导致数据丢失。使用 ILM(Index Lifecycle Management)自动滚动、冷热迁移、删除过期索引。

高频追问
  • Q: 为什么 ES 不适合实时强一致性场景?A: ES 是近实时(near-realtime),默认 1s refresh 间隔。且多副本异步复制,存在写入成功但搜索不可见或副本延迟的窗口。不适合替代数据库做事务。
  • Q: 深度分页如何优化?A: from+size 超过 10000 会触发深度分页,消耗大量内存。使用 search_after(实时游标)或 scroll(快照游标),或限制用户翻页深度。
Elasticsearch 分片 段合并 调优
E10.03
HNSW vs IVF 向量索引:分层小世界图 vs 倒排聚类、召回率/内存/构建成本取舍、RAG 选型决策架构P2
核心要点
  • HNSW(Hierarchical Navigable Small World):分层小世界图,顶层稀疏快速导航、底层稠密精确定位;查询复杂度 O(log N)
  • IVF(Inverted File):基于 K-Means 聚类倒排,查询时只扫 top-nprobe 个簇;复杂度 O(N/nlist)
  • 召回率:HNSW 召回率 95%+ 同等查询时显著优于 IVF;IVF 召回率随 nprobe 增加提升但 RT 上升
  • 内存占用:HNSW 每向量存储图连接(M 倍指针),内存是 IVF 的 1.5~3 倍;IVF 仅存聚类中心 + 倒排表
  • 适用场景:HNSW 适合中小规模(< 1 亿)+ 高召回;IVF 适合超大规模(亿~十亿)+ 可接受召回损失 + 配合 PQ 量化
详细解答

HNSW 结构:多层图,每层节点数按指数衰减(层 i 节点数 ≈ N × e^(-i));查询从顶层贪心搜索到目标层,再下沉到底层精确召回。核心参数:M(每节点连接数,16~48)、efConstruction(构建时邻居候选数,200~500)、efSearch(查询时邻居候选数,50~200)。M 越大召回越高内存越大,efSearch 越大召回越高 RT 越长。

IVF 结构:K-Means 训练 nlist 个聚类中心;向量按最近中心分桶;查询时计算 query 到所有中心距离,选 top-nprobe 个桶扫描。核心参数:nlist(桶数,sqrt(N)~N/1000)、nprobe(查询桶数,8~64)。nprobe 越大召回越高但 RT 越长。

性能对比:1000 万 768 维向量,HNSW 召回 95% RT ~5ms,IVF nprobe=8 召回 85% RT ~8ms;原始向量本身约 30GB(1000万×768×4B);HNSW 总内存约 35GB(原始向量 30GB + 图结构约 5GB);IVF 若不量化约 31GB,配 PQ 量化后约 1~2GB。HNSW 在召回率与 RT 平衡上明显占优,代价是内存。

量化压缩:IVF 常配 PQ(Product Quantization)将 768 维压缩到 96 字节,内存降 50 倍但召回率下降 5%~10%;HNSW 配 PQ 需重排(re-rank,先取 top-k 候选用压缩向量,再用原始向量精排)保留高召回。索引构建:HNSW 构建慢(O(N log N) 全量插入),增量更新成本高(需维护图连通性);IVF 构建快(K-Means 一次性),增量只需重算聚类中心。

工程实践

选型决策:规模 < 1000 万 + 强召回 → HNSW;规模 > 1 亿 + 可量化 → IVF-PQ;超大规模 + 实时 → IVF + 分片 + GPU 加速(Faiss-GPU/Milvus)。主流引擎:Milvus(同时支持 HNSW/IVF/ANNOY/DiskANN)、Faiss(IVF-PQ 标杆)、Qdrant(HNSW 优化)、Elasticsearch 8.x(dense_vector + HNSW)。参数调优:HNSW 调 M(16~48)/ efConstruction(200~500)/ efSearch(50~200);IVF 调 nlist(sqrt(N)~N/1000)/ nprobe(8~64)。增量更新:HNSW 用"软删除 + 标记位"避免图重建;IVF 直接插入到最近桶,定期重训聚类中心。混合检索:向量召回 + BM25 关键词召回 + RRF(Reciprocal Rank Fusion)重排,是 RAG 生产标准范式。

高频追问
  • Q: HNSW 为什么比 IVF 召回高?A: 图结构保留向量邻接关系,贪心搜索可跨"桶"跳跃;IVF 受限于聚类边界,边界附近向量召回率低(可能归到相邻桶但 nprobe 没覆盖)。
  • Q: HNSW 内存为什么这么大?A: 每向量需存 M 个邻居指针(每指针 4~8 字节),M=32 时每向量额外 128~256 字节;亿级向量图结构本身 10~25GB,加上原始向量内存翻倍。
  • Q: 向量索引能像 B+ 树支持范围查询吗?A: 不能直接支持。向量检索是 ANN(近似最近邻),范围查询需配合过滤(pre-filter / post-filter);Milvus/Qdrant 支持 metadata 过滤,pre-filter 精确但慢,post-filter 快但可能召回不足。
  • Q: RAG 场景下 HNSW 和 IVF 怎么选?A: 知识库 < 100 万 → HNSW(毫秒级高召回);100 万 ~ 1000 万 → HNSW + 分片;> 1000 万 → IVF-PQ + 重排;超大规模考虑 DiskANN(磁盘索引)降低内存压力。
向量检索 HNSW IVF RAG
E10.04
ES Mapping 与动态模板:text/keyword、分词器与字段爆炸治理架构P1
核心要点
  • text 可分词检索;keyword 精确聚合/排序;常双字段
  • 动态映射易字段爆炸;生产用严格 mapping + dynamic模板
  • 中文分词:IK 等插件;同义词/停用词进版本化配置
  • 变更 mapping 多数字段不可就地改类型,需 reindex
  • 深挖:E10.01/E10.02;篇章 C11 ES
详细解答

Mapping 是搜索相关性与聚合正确性的合同。未知字段用 dynamic: strict 或模板归到 keyword,避免海量 field 拖垮集群状态。

工程实践

索引模板 + ILM;改分词走新索引影子切换;监控 field 数量与 mapping 更新。

高频追问
  • Q: 为何 keyword 不做全文?A: 整值单一 token,适合过滤聚合,不适合分词召回。
  • Q: 能否改已有字段类型?A: 多数不行;需新索引 + reindex。
Mapping 分词 ES
E10.05
BM25 与相关性调优:k1/b、字段加权与业务信号融合高级P1
核心要点
  • BM25:TF 饱和 + IDF + 长度归一;默认 k1≈1.2、b≈0.75
  • 调参:长文本字段降 b;标题等高权字段 boost
  • 功能分:业务质量分/时效 decay 与 BM25 加权融合
  • 评估:NDCG/人工标注;忌只调感观
  • 深挖:E10.01;RAG 混合检索见 E10.03/E10.06
详细解答

纯词法相关不够时,用 function_score 或检索后重排。调 k1/b 前先修分词与同义词,否则参数无效。

工程实践

离线评测集固化;每次调参 A/B;记录查询形状(短词/长句)分别出报告。

高频追问
  • Q: k1 太大怎样?A: 词频影响更线性,易被堆词文档占优。
  • Q: BM25 能替向量吗?A: 不能;关键词精确与向量语义互补,生产常混合。
BM25 相关性 ES
E10.06
RAG / 向量同步速查:双写一致性、重建切换与评测门禁架构P1
核心要点
  • 主存仍是业务 DB;向量库是派生索引,须可重建
  • 同步:Outbox/CDC → 嵌入任务;禁止无补偿双写
  • 切换:影子索引 + 流量镜像 + 一键回滚
  • 门禁:召回率/延迟/幻觉评测;Prompt/模型版本绑定
  • 深挖:E10.03;C12 RAG/Agent;G 卡 AI 评测
详细解答

向量滞后窗口要对业务可见(近实时承诺)。删除/更新必须进同步管道,避免「DB 已删检索仍命中」。

重建当发布:版本化 collection、双跑对比、达标再切主。

工程实践

同步 lag 告警;重建剧本与回滚;评测集随知识库变更回归。禁止只看写入吞吐。

高频追问
  • Q: 为何不把向量库当主库?A: 事务/关联/审计弱;应以 DB 为准可重建。
  • Q: 双写最常见事故?A: 一边成功一边失败无补偿,导致长期不一致。
RAG 向量 一致性

十一、云原生与运维

本组知识链

本组把运行时交付收成:容器隔离 → K8s 工作负载与弹性 → Prometheus 指标体系 → OpenTelemetry 三支柱与采样 → FinOps 成本杠杆。

  1. 容器基础:Namespace/Cgroup/联合文件系统与镜像分层。
  2. 编排:Pod 调度、滚动更新、HPA、Service 网络。
  3. 可观测:指标/告警/PromQL;OTel 传播、Collector、尾部采样。
  4. 成本:计量归因、单位成本、预算告警与工程降本杠杆。
工程化要点
  • 资源 request/limit 与 JVM/池化预算同一容量模型。
  • 滚动更新与就绪/存活探针必须可证明流量切干净。
  • HPA 冷却与防抖参数进大促清单。
  • 告警规则可行动并绑定 Runbook;禁止纯观赏仪表盘。
  • Trace 采样策略平衡成本与关键路径覆盖。
  • 镜像签名与漏洞扫描进部署门禁。
  • FinOps:单位成本看板与预算告警默认开启。
本组思维模式
  • 编排≠容量:副本数绿不代表池子与依赖够。
  • 探针是契约:错误探针制造发布与自愈事故。
  • 遥测有成本:采样与基数爆炸要治理。
  • 成本是可靠性维度:失控账单会倒逼错误降级。
  • 交付可回滚:镜像、配置、流量三维可退。
E11.01
Docker:Namespace、Cgroups、联合文件系统、网络模式与镜像分层高级P2
核心要点
  • Namespace:PID(进程隔离)、NET(网络隔离)、IPC(信号量/共享内存隔离)、MNT(挂载点隔离)、UTS(主机名隔离)、USER(UID/GID 隔离),实现容器间资源视图隔离
  • Cgroups:限制容器使用的 CPU(cpu-shares/cpu-quota)、内存(memory.limit_in_bytes)、IO(blkio.throttle)、PID 数量等资源,防止单容器耗尽宿主机资源
  • 联合文件系统(UnionFS):镜像分层存储,每层只读,容器启动时叠加可写层(Copy-on-Write)。Overlay2 是目前主流驱动,性能优于 AUFS 和 Devicemapper
  • 网络模式:bridge(默认,独立网桥与宿主机 NAT 通信)、host(共享宿主机网络栈,性能高但端口冲突)、none(无网络)、container(共享其他容器网络栈)
  • 镜像构建:Dockerfile 每条指令生成一层,层数过多导致镜像体积大、构建慢。优化:多阶段构建、合并 RUN 命令、使用更小基础镜像(alpine/distroless)
详细解答

Namespace 隔离并非绝对安全:容器与宿主机共享内核,内核漏洞(如 Dirty COW、 privilege escalation)可导致容器逃逸。因此生产环境应避免以 --privileged 运行容器,并启用 SELinux/AppArmor。

Cgroups v1 vs v2:v1 每个子系统独立挂载(cpu/、memory/),v2 统一层级并使用 BPF 优化。v2 支持更精细的内存回收和 IO 控制,但部分旧工具尚未完全兼容。

Overlay2 写时复制:容器修改文件时,文件从镜像层复制到可写层(upperdir),后续修改只在可写层进行。删除文件时通过 whiteout 机制标记,不真正删除底层文件。这导致大量小文件写操作性能下降。

Docker 网络数据流:bridge 模式下容器 eth0 连接 veth pair 一端,另一端在宿主机 bridge(docker0)上。容器访问外网时,源 IP 经 iptables MASQUERADE 做 SNAT 转换为宿主机 IP。

工程实践

Java 容器化注意:默认 JVM 按宿主机内存设置堆大小,需设置 -XX:MaxRAMPercentage。Docker 的 memory limit 只限制 RSS,不限制堆外内存(DirectByteBuffer、Metaspace),需同时设置 -XX:MaxDirectMemorySize。健康检查建议使用 Spring Boot Actuator /actuator/health。

高频追问
  • Q: 容器与虚拟机的本质区别?A: 容器共享宿主机内核,轻量、启动快(秒级);虚拟机有独立内核和硬件抽象,隔离性强但资源开销大、启动慢(分钟级)。
  • Q: 如何缩小 Java 镜像体积?A: 多阶段构建(Maven 编译与运行分离)、使用 jlink 裁剪 JRE、使用 distroless 或 alpine 基础镜像、删除不必要的依赖和文件。
Docker Namespace Cgroups 容器
E11.02
Kubernetes:Pod 调度、Deployment 滚动更新、HPA、Service 与网络模型架构P2
核心要点
  • Pod 调度:Scheduler 通过预选(Predicates:资源充足、节点亲和性、污点容忍)和优选(Priorities:资源平衡、亲和性权重)选择最优节点
  • Deployment 滚动更新:通过 ReplicaSet 控制副本,更新时创建新 RS 并逐步扩容,旧 RS 缩容,默认 maxUnavailable=25%、maxSurge=25%
  • HPA(Horizontal Pod Autoscaler):基于 CPU/内存利用率或自定义指标(Prometheus Adapter)自动扩缩 Pod 副本数,冷却窗口默认 5min 防止抖动
  • Service:ClusterIP(集群内访问)、NodePort(节点端口暴露)、LoadBalancer(云 LB 集成)、Headless(直接返回 Pod IP,用于有状态服务)
  • 网络模型:每个 Pod 有独立 IP(扁平网络),同节点内通过 veth + bridge 通信,跨节点通过 CNI(Calico/Flannel/Cilium)隧道或 BGP 路由
详细解答

调度流程:① 用户创建 Pod → 写入 etcd;② Scheduler Informer 感知 → 执行预选过滤不满足条件的节点 → 优选打分 → 绑定(Bind)到目标节点;③ 目标节点 kubelet 感知 → 调用 CRI/CNI/CSI 创建容器。调度器可扩展自定义调度框架(Scheduling Framework)。

滚动更新策略:Recreate(先删后建,有停机时间)、RollingUpdate(默认)。通过 kubectl rollout status/status undo 查看/回滚。注意 readinessProbe 配置不当可能导致新 Pod 未就绪就继续滚更新,引入流量到未准备好的实例。

Service 的 kube-proxy 实现:userspace(已废弃)、iptables(默认,O(n) 规则遍历)、ipvs(内核级,O(1) 哈希,适合大集群)。IPVS 支持更多负载均衡算法(rr/lc/dh/sh/sed/nq)。

有状态服务:StatefulSet 提供稳定的网络标识(Pod 名-序号)和持久存储(PVC 按序创建和挂载),配合 Headless Service 实现分片发现。适合 MySQL、Redis、Kafka、ZooKeeper 等。

工程实践

生产集群建议:Master 节点至少 3 台实现高可用( stacked etcd 或外部 etcd)。工作节点配置资源预留(kube-reserved/system-reserved)。使用 PodDisruptionBudget 保证滚动更新/驱逐时最少可用副本。日志采集推荐 DaemonSet 部署 Fluent Bit/Logstash,指标采集使用 Prometheus + node-exporter + kube-state-metrics。

高频追问
  • Q: Pod 一直处于 Pending 如何排查?A: kubectl describe pod 查看 Events。常见原因:资源不足(Insufficient CPU/Memory)、节点亲和性/污点不匹配、PVC 未绑定、镜像拉取失败、调度约束过严。
  • Q: 如何实现蓝绿发布或金丝雀发布?A: 蓝绿:两个 Deployment 切换 Service Selector;金丝雀:Ingress-Nginx/Service Mesh(Istio)按权重或 Header 分发流量,逐步切流验证。
Kubernetes Pod HPA Service
E11.03
Prometheus + Grafana:指标体系、Exporter、告警规则、PromQL 与可观测性高级P2
核心要点
  • 数据模型:时序数据库,以 metric_name{label1="v1",label2="v2"} 标识时间序列,支持 Counter(单调递增)、Gauge(可增可减)、Histogram(分桶统计)、Summary(分位数统计)四种类型
  • 拉模式(Pull):Prometheus Server 主动抓取 Exporter /应用端点(/metrics),相比推模式更易控制采集节奏、便于排查单个目标故障
  • PromQL:支持瞬时向量选择、范围向量([5m])、聚合操作(sum/by/without)、二元运算、内置函数(rate/irate/increase/histogram_quantile)
  • 告警链路:Prometheus 根据 rule 文件计算告警 → 发送给 Alertmanager → 去重、分组、抑制、静默 → 路由到钉钉/企业微信/PagerDuty
  • Exporter:node_exporter(主机指标)、jmx_exporter(Java 指标)、mysql_exporter、redis_exporter、blackbox_exporter(探测)
详细解答

rate() vs irate():rate 计算范围向量内的平均每秒增长率,适合平滑曲线;irate 使用最后两个样本计算瞬时率,对突发更敏感但波动大。监控磁盘/CPU 等变化缓慢的指标用 rate,请求 QPS 用 irate。

Histogram vs Summary:Histogram 在客户端分桶(如 0.005s, 0.01s, 0.025s...),服务端可灵活计算任意分位数,但桶的数量和范围需预先定义;Summary 在客户端直接计算分位数(如 0.99),精度高但无法聚合多个实例的分位数。

高可用方案:Thanos(Sidecar + Query + Store + Compact)实现 Prometheus 水平扩展和长期存储;或 VictoriaMetrics/Cortex 作为兼容 remote_write 的替代存储。联邦集群(Federation)适合层级汇聚,但不适合海量指标。

Java 应用接入:Micrometer 作为埋点门面,自动暴露 JVM GC/内存/线程/日志指标,并适配 Prometheus 格式。配合 Spring Boot Actuator 的 /actuator/prometheus 端点,无需额外开发即可获取丰富指标。

工程实践

告警设计遵循 Google SRE 的减少噪音原则:优先使用 SLO/SLI 驱动告警(如错误率 > 1%、P99 延迟 > 500ms 持续 5min),避免 CPU 波动等易抖动指标直接告警。Grafana 仪表盘按服务/层级组织,关键大盘置顶。指标基数(Cardinality)控制:避免将用户 ID、订单 ID 等高频变化值作为 label。

高频追问
  • Q: Prometheus 的拉模式有什么缺点?A: 不适合短生命周期任务(Batch Job),需通过 Pushgateway 中转;防火墙/NAT 后目标需暴露端口;采集间隔内数据丢失无法回溯。
  • Q: 高基数(High Cardinality)问题如何解决?A: 减少 label 数量,避免无界枚举值(如 IP、UUID)。若必须,使用 recording rule 预聚合降低查询维度,或采用日志系统替代指标存储细节。
Prometheus Grafana 监控 可观测性
E11.04
OpenTelemetry 三支柱与采样:Traces/Metrics/Logs 关联、W3C 传播、Collector 与尾部采样架构P1
核心要点
  • 三支柱:Traces(调用链)、Metrics(聚合数值)、Logs(事件文本);OTel 用同一 Context 关联
  • 传播:W3C traceparent;Java Agent 无侵入 + 边界补全(线程池/MQ 头/定时任务重建根)
  • 管道:SDK/Agent → OTLP → Collector(批处理/尾采样/导出)→ 后端(Tempo/Jaeger/ClickHouse…)
  • 采样:头部采样控成本;尾部采样保错误/慢请求;关键链路可全量
  • 与 E11.03 分工:Prometheus 偏指标拉模式;本卡偏标准可观测协议与 Trace 关联。落地互链 C11.09、C10.22
详细解答

为何 OTel:厂商 API 锁定(OpenTracing/OpenCensus 分裂)→ OTel 统一语义约定与 SDK,后端可换。OpenTelemetry 于 2026-05 宣布 CNCF 毕业,进一步巩固"可观测标准层"定位;生产选型仍看 Collector/采样/成本,而非只看毕业标签。

Trace 模型:Trace = Span 树;Span 含属性、事件、状态。跨服务靠 context propagation;异步必须显式传递,否则断链。

采样策略:① Head:入口决定,简单但可能漏异常慢请求;② Tail:先收全再判,保 SLO 违约样本,Collector 内存/缓冲成本更高;③ 混合:默认 5%–10% + 错误/高延迟强制保留。

Logs 关联:日志注入 trace_id/span_id;禁止把高基数业务 ID 当 metric label(基数爆炸)。

工程实践

Java 优先 OpenTelemetry Java Agent;Spring Boot 3 用 Micrometer Tracing 桥接。网关校验/重建非法 traceparent。Collector 做敏感属性脱敏与导出限流。黄金信号(延迟/流量/错误/饱和)告警,避免 CPU 抖动直告。

采样默认表(跨卡真源):

场景 头部采样 尾部/强制保留
稳定生产 约 1% 错误 + 慢请求(如 >SLO)全留
灰度/排障窗口 约 10% 同上,可临时提高
压测/演练 可达 100% 注意存储与 Collector 容量

S08.06/07/08、C10.22 表述以本表为准。勿把「默认 5%~10%」与「稳定期 1%」混为一谈——前者多为排障窗口口径。

Collector 拓扑一句:边缘 Daemon/Sidecar 收 OTLP → 中心 Collector 批处理/尾采样/脱敏 → 多后端;业务禁止直写 Tempo/ES(见 S08.08)。

高频追问
  • Q: 有了 Prometheus 为何还要 OTel Trace?A: Metrics 回答「坏了多少」;Trace 回答「哪一跳坏了」。二者互补,OTel 可同时导出。
  • Q: 线程池为何丢 Trace?A: Context 在 ThreadLocal;提交任务需包装(TaskDecorator / Context.wrap),否则子线程无 parent span。
  • Q: 尾部采样一定优于头部?A: 否。尾部更准但贵;流量极大时用分层:入口粗采样 + 错误全保留。
  • Q: 稳定期为什么推荐 ~1% 而不是 10%?A: 成本与基数;错误/慢请求靠尾部强制保留补齐,不必用高头部采样换「安全感」。
OpenTelemetry Tracing 采样 可观测
E11.05
FinOps 原理速查:计量归因、单位成本、预算告警与工程杠杆架构P1
核心要点
  • FinOps = 云/AI 成本的工程化治理:可见 → 归因 → 优化 → 运营闭环
  • 计量:资源(CPU/GPU/存储/egress)+ 业务标签(租户/产品/环境);LLM 另计 Token
  • 单位成本:¥/请求、¥/千 Token、¥/活跃用户;只看账单总额无法决策
  • 杠杆:权利化(预留/Spot)、架构(缓存/批量化)、产品(配额/降级)
  • 互补:中间件成本卡 C11.20;场景 S11.06
详细解答

为何要原理卡:篇章讲中间件账单与优化清单;本卡固化「标签体系 + 单位经济学 + 告警闭环」心智,避免只背省钱技巧。

归因模型:云账单 → 命名空间/标签 → 服务 → 业务线。缺标签即「无主成本」。K8s 用 namespace+label;LLM 网关必须打 business/tenant/model。

优化顺序:先砍浪费(闲置、过量 limits、未关环境)→ 再架构(缓存、异步、分级模型)→ 最后商务(预留折扣)。

与 SLO:成本优化不得默默伤延迟/可用性;变更带质量回归与回滚(见 S11.06)。

工程实践

日预算阈值 + 异常检测(环比/同比);Token 限流与 QPS 限流并列。看板最小集:总花费、Top 服务、单位成本趋势、闲置资源清单。GPU/大模型单独账本。

高频追问
  • Q: FinOps 是财务还是研发的事?A: 共建。财务定预算;工程负责标签、计量与可回滚优化。
  • Q: 为什么单位成本比总额重要?A: 业务增长时总额上升正常;单位成本恶化才是效率问题。
  • Q: 先砍机器还是先砍 LLM?A: 看占比与弹性。LLM 常「陡峭且可路由」;闲置集群是「白扔」优先砍。
FinOps 成本归因 单位成本 Token

十二、数据结构与算法

本组知识链

本组把算法收成工程选型与模板:线性结构 → 树与索引 → 跳表布隆 → 缓存淘汰与一致性哈希 → 排序与高频模板 → 图/DP/贪心边界。

  1. 线性与哈希:数组/链表/栈队列与 HashMap 复杂度。
  2. 树与索引:BST/B/B+ 与数据库索引同构直觉。
  3. 概率与缓存:SkipList、BloomFilter、LRU/LFU、一致性哈希。
  4. 模板与边界:排序稳定性、双指针滑动窗口、图遍历与 DP/贪心适用面。
工程化要点
  • 结构选型写进设计说明:复杂度、内存、并发友好性。
  • 布隆过滤器误判率与重建策略进缓存穿透方案。
  • 本地缓存淘汰策略与容量评审绑定。
  • 一致性哈希虚拟节点与数据迁移方案可演练。
  • 算法题模板化为可复用代码片段,边界用例进单测。
  • 排序/优先队列选型对照稳定性与额外空间。
  • 热点路径避免错误结构导致的隐性 O(n²)。
本组思维模式
  • 选型先于题感:先问访问模式与约束。
  • 与系统同构:B+ / 跳表 / 哈希在中间件中有直接对应。
  • 概率结构有误判:Bloom 等必须设计补救路径。
  • 模板固化边界:二分与窗口题死在边界条件。
  • 贪心慎用:先证明最优子结构再贪心。
E12.01
Java 常用数据结构选型:数组 / 链表 / 栈 / 队列 / HashMap 底层与复杂度高级P0
核心要点
  • 数组:随机访问 O(1),插入删除 O(n)(需搬移),扩容为新数组拷贝;适合读多写少、下标访问
  • 链表:插入删除 O(1)(给定节点),随机访问 O(n),节点分散、缓存不友好;适合频繁增删
  • 栈/队列:栈 LIFO(JVM 方法栈、括号匹配、深度优先),队列 FIFO(BFS、任务缓冲);Java 常用 ArrayDeque 双端队列实现
  • HashMap:数组 + 链表/红黑树,负载因子 0.75,默认 16,扩容 2 倍;链表长度 ≥8 转红黑树、≤6 退回链表,减少极端哈希冲突
  • 有序结构:TreeMap(红黑树,按键有序)、LinkedHashMap(保插入/访问序,可做 LRU);ConcurrentHashMap 采用数组 + CAS + 红黑树并发安全
详细解答

ArrayList 底层 Object[],默认容量 10,扩容按 1.5 倍并 System.arraycopy;LinkedList 单/双链表,头尾操作 O(1) 但中间访问 O(n)。大量随机访问优先 ArrayList,其数组连续内存利于 CPU 缓存局部性。

HashMap 1.7 用头插法,并发下扩容可能形成环链表导致死循环;1.8 改为尾插法并引入红黑树,虽仍非线程安全,但避免了死链。put 流程:hash 扰动(高 16 位异或低 16 位)→ 定位桶 → 空则直接放,冲突则链/树化;树化要求同时满足容量 ≥64。

LinkedHashMap 重写 get/put 的 hook,accessOrder=true 时每次访问把节点移到链表尾部,天然实现 LRU 雏形;最老节点在头部可驱逐。TreeMap 所有操作 O(log n),适合区间/顺序遍历需求。

工程实践

选型口诀:需要下标/极致读性能用数组;频繁增删且不关心顺序用链表(或 ArrayDeque 模拟);键值查找用 HashMap 将 key → bucket 索引;需要有序/顺序遍历用 LinkedHashMap 或 TreeMap。并发写场景用 ConcurrentHashMap 而非同步包装的 Hashtable。

高频追问
  • Q: 为什么树化阈值选 8?A: 基于泊松分布,负载因子 0.75 时链表长度达 8 的概率已低于千万分之一,此时是哈希函数设计有问题的信号,树化兜底极端冲突。
  • Q: 负载因子为什么取 0.75?A: 取 0.75 是时间(越少碰撞次数越少)与空间(越大扩容越迟)的折中;过小浪费空间,过大冲突增多。
  • Q: ArrayList 与 LinkedList 谁更省内存?A: ArrayList 更省,只是连续对象引用;LinkedList 每个节点额外存前后指针,且节点分散导致缓存命中率低。
数据结构 HashMap 集合 复杂度
E12.02
树与索引:二叉树 / BST / B 树 / B+ 树,为何 MySQL 用 B+ 树专家P0
核心要点
  • 二叉树:无序;BST:左小右大,平均 O(log n),但插入有序数据退化为链表 O(n)
  • 平衡树:AVL 严格平衡(旋转多)、红黑树近似平衡(旋转少、最多 2log(n+1)),删除场景红黑树更优
  • B 树:多路平衡树,节点多个键和子树,同一节点存数据,减少磁盘 IO 次数(层级低)
  • B+ 树:内节点只存键做路由,数据全在叶子;叶子间以链表相连支持范围扫描
  • MySQL InnoDB 聚簇索引用 B+ 树:扇出大、高度低、叶子链表利范围扫——引擎侧细节与回表/覆盖索引见 E05.01,本卡只保留选型对比,避免与 E05 重复展开
详细解答

为什么是 B+ 树而非 B 树:① B+ 树内节点不含数据,一个页可存更多键,扇出更大、树更矮、IO 更少;② 数据都集中在叶子且有序,范围查询、ORDER BY、GROUP BY 直接沿叶子链表遍历,无需回溯中序遍历;③ 叶子节点可做预读聚簇,缓存命中率高。

为什么不用哈希索引:哈希只支持等值 O(1),不支持范围查询与排序(MongoDB 等用 Hash);为什么不用红黑树:红黑树是二叉(扇出小,高度高),存储于磁盘时树高意味着更多次磁盘 IO,B+ 树单节点多路显著压低高度。

主键选择对聚簇索引的影响:InnoDB 主键即聚簇索引,自增整形主键顺序写入、避免页分裂与碎片;UUID 乱序主键导致大量随机插入、频繁页分裂与碎片。二级索引叶子存主键值回表。

工程实践

索引设计:前缀索引、复合索引遵循最左前缀;覆盖索引避免回表并可下推(ICP);区分度低的列(性别)不适合建索引。大批量写入用自增主键;磁盘存储引擎上线前评估索引扇出。

高频追问
  • Q: B 树与 B+ 树在数据存储上的核心差异?A: B 树所有节点都可存数据、且只有一个 Key 一个值;B+ 树仅叶子存数据并构成有序链表,内节点只是索引路由。
  • Q: 为什么 UUID 主键会降低写入性能?A: UUID 无序导致插入随机落在已有页,触发频繁页分裂,B+ 树叶子页乱序、索引碎片化,扇出与命中率下降。
树 索引 B+树 数据库
E12.03
跳表 SkipList 与布隆过滤器 BloomFilter 原理与典型应用高级P1
核心要点
  • 跳表:有序链表之上叠加多层索引,逐层跳跃查找,期望 O(log n),支持范围查询与 O(log n) 插入删除,实现简单、天然有序
  • 典型应用:Redis 有序集合(zset)底层用跳表,因为实现简单、易支持区间和 ZRANGE 排名场景
  • 布隆过滤器:位数组 + k 个哈希函数,插入把 k 个位置置 1,查询 k 个位置是否全 1
  • 结论语义:返回「不存在」是必定不存在;返回「存在」仅可能存在(有误判);无法删除元素
  • 典型应用:缓存穿透防御、URL/黑名单去重、数据库防读穿透、分布式系统判重
详细解答

跳表查找:从最高层指针对应小与目标则右移、大则下降一层,直到底层定位;新区插入按概率 p(通常 0.5)决定层高随机生成。相比红黑树跳表不需频繁旋转,代码简单,适合内存中的有序结构。

布隆过滤器误判率与参数关系:位数组大小 m 越大误判越低;元素 n 越多误判越高;哈希函数个数 k 最优取 m/n·ln2。一次查询与插入都是 O(k),空间远小于存整元素。无法删除是最大不足,可引入计数布隆过滤器(每个槽计数)支持删除,但占用更多空间。

工程中常与缓存结合:请求先过布隆过滤器,若不存在则直接返回,避免大量不存在 key 打到 DB(缓存穿透);元素会被定期重建以吸收新增数据。

工程实践

防缓存穿透:把合法 key 集合建布隆过滤器(Guava/Redisson),拦截不存在的 key;配合空值缓存 + 过期时间兜底。跳表适合需要有序 + 区间的内存结构,分布式排序结构或对写敏感的索引可优先考虑。

高频追问
  • Q: Redis zset 为什么用跳表而不用红黑树?A: 跳表实现简单、修改不需大幅旋转,且天然支持按序区间遍历;红黑树需额外维护顺序遍历复杂度更高,工程取舍。
  • Q: 布隆过滤器误判会带来什么问题?A: 会把少量不存在的 key 误判为存在而放行去查 DB,代价是「多一次无害的 DB 查询」,不会漏过真实存在的数据。
跳表 布隆过滤器 Redis 缓存
E12.04
LRU / LFU 缓存结构设计与一致性哈希高级P1
核心要点
  • LRU:最近最少使用;实现 = 双向链表 + HashMap,get/put 均 O(1);命中节点移到表头,表尾为最久未用可驱逐
  • LFU:访问频率最低优先淘汰,对抗「一次突发不常用」的缓存污染,但实现更复杂,需按频率维护桶
  • Java 实现:LinkedHashMap 设置 accessOrder=true 天然做 LRU;并发可用 ConcurrentHashMap + 自管理或直接用 Redis 淘汰策略
  • 一致性哈希:把 key 与节点映射到哈希环,顺时针找最近节点;加入虚拟节点使分布更均匀,增删节点只影响相邻一段 key,迁移量小
  • 典型应用:本地缓存/HTTP 缓存淘汰、Redis maxmemory-policy allkeys-lru、分布式缓存节点寻址
详细解答

LRU 操作:get 命中把节点移到头部;put 时若容量满则移除尾节点(最久未用)再插入头部。双向链表的相邻节点引用在换出时 O(1) 完成,配合 HashMap 的 key→节点映射实现 O(1) 定位,否则链表查找是 O(n)。

LFU vs LRU:LFU 统计累计访问频率,能抵抗「一次性热点」污染,但对访问模式变化不敏感(旧高频项难被换出);LRU 只关心最近访问时间,实现简单、成本低。微服务本地缓存、数据库缓冲池常用近似 LRU(如 MySQL 自适应哈希 + LRU 分代)。

一致性哈希在哈希环分布有数据倾斜问题时,通过为每个物理节点加若干虚拟节点(映射到环上多个位置)使负载更均匀;删除/新增节点仅重哈希该节点到下一节点之间的 key,命中率波动小而稳定。

工程实践

选型:读热点稳定、又要防污染 → LFU;实现简单、写多读少 → LRU。分布式缓存路由避免直接取模(%N),取模在节点增减时几乎所有 key 都会重新映射,一致性哈希把影响范围缩到最小。

高频追问
  • Q: 如何用 O(1) 实现 LRU?A: HashMap 存 key→Node,Node 组成双向链表;get 命中节点摘除插头部,put 满时删尾部,全部 O(1)。
  • Q: 一致性哈希为什么能减少缓存迁移?A: 节点在环上只有加入/移除位置相邻的一段 key 会切换目标节点,其余 key 的映射不变,命中率不受全局影响。
LRU LFU 一致性哈希 缓存
E12.05
排序算法:复杂度 / 稳定性、快排 / 归并 / 堆排,Java 排序的选用高级P2
核心要点
  • O(n²):冒泡(稳定)、选择(不稳定)、插入(稳定);数据规模小/局部有序时插入表现好
  • 快排:分治就地交换,平均 O(n log n),最坏 O(n²),不稳定;常数小、速度快
  • 归并:分治 + 合并,稳定,需 O(n) 额外空间,适合外部排序与链表排序
  • 堆排:利用最大/最小堆,O(n log n),不稳定,就地、最坏仍 O(n log n)
  • Java:Arrays.sort 基本类型用双轴快排(DualPivotQuickSort),对象类型用归并(TimSort),保证稳定性与友好数据
详细解答

稳定性意义:需要多级排序时(先按时间再按优先级)稳定性保证同优先级元素保持原始相对顺序。快排不稳定因交换会打乱相等元素次序;归并稳定因合并时等值取左序列。

快排最坏退化的避免:基准选择用随机化或三数取中;当递归区间很小时改插入排序。归并适合无法原地(链表、外存)的场景,外部排序(海量数据不落内存)用归并多路合并 + 败者树。

Java 对象数组用 TimSort——归并的改良,对已排序/接近有序数据近乎 O(n),且稳定;所以自定义对象用 Collection.sort 结果稳定、基本类型排序不稳定。

工程实践

手写常考快排/归并模板;生产直接交给标准库。海量数据(GB 级)排序走外部排序:分块读入内存排好写临时文件,再 K 路归并,配合败者树减少比较。稳定优先用归并,性能优先用快排。

高频追问
  • Q: 快排最坏情况是什么时候?A: 每次基准都选到极值(如已有序数据仍固定取首元素)时退化为 O(n²);随机化或三数取中可避免。
  • Q: 稳定的排序算法有哪些?A: 冒泡、插入、归并稳定;选择、快排、堆排不稳定。
排序算法 复杂度 稳定性 算法
E12.06
链表 / 数组高频算法:反转、环形检测(快慢指针)、合并有序序列高级P1
核心要点
  • 反转链表:迭代(prev/curr/next 三指针)与递归(先反转后半再拼头)两种模板,迭代 O(1) 额外空间
  • 环形链表:快慢指针,快指针每次走两步、慢一步,若相遇则有环,能证明且可求环入口
  • 合并两个有序链表:虚拟头节点 + 双指针逐个取小;数组版归并同理
  • 删除倒数第 N 个节点:双指针先走 N 步再同步走,一次遍历定位
  • 数组场景:三数之和/两数之和用排序 + 双指针对撞;原地去重用快慢指针
详细解答

反转链表核心是不丢失对后继的引用:先把 next 暂存,再让当前节点指向前驱,然后前驱与当前整体前移。递归写法从尾到头逐层让下一节点指向自己,最后新头是原尾节点。

快慢指针判环:两者在某时刻相遇说明 slow 已经「转圈」追上 fast;求环入口——相遇后让一指针回原点、二者同速走,再次相遇点即环入口(步数关系:起点到环口 = 相遇点到环口)。时间 O(n)、空间 O(1),优于哈希表记录已访问节点。

合并有序链表设置 dummy 哨兵节点避免讨论头节点为空的情况,最后直接返回 dummy.next;两数之和排序后用头尾指针,小于目标左移右、大于目标右移左。

工程实践

这些是手敲高频题,建议把「反转、判环、合并、找中点(快慢)、删除倒数第 k」背成模板;重点关注边界(空链表、单节点、k>长度)处理是否健壮。

高频追问
  • Q: 快慢指针判环为什么要快走两步?A: 若快每次也走一步永远追不上;走两步保证在有限步内追上慢指针(环内相对速度 1 步/轮,不会跳过)。
  • Q: 递归反转的空间复杂度?A: O(n),因递归栈深度为链表长度;迭代只 O(1),海量链表用迭代。
链表 双指针 快慢指针 算法题
E12.07
二分查找、双指针、滑动窗口的模板与边界处理高级P1
核心要点
  • 二分查找前提:有序(或满足单调性);mid 用 mid = lo + (hi - lo) / 2 防溢出
  • 三类模板:找精确值(lo <= hi)、找左边界、找右边界;边界收缩不同,易错点在于是否会死循环或漏查
  • 双指针对撞:有序数组两数之和、三数之和、接雨水等,O(n)
  • 滑动窗口:定长与不定长(维护窗口内满足条件),通常 O(n);经典为无重复最长子串、最小覆盖子串
  • 二分亦可应用于旋转数组查找、有序矩阵搜索、二分答案(最值可行性判断)
详细解答

二分模板关键统一:精确查找 while (lo <= hi) 时 mid 命中即返回;找边界用左闭右开 [lo, hi),循环不变量是「lo 侧恒小于目标、hi 侧恒大于等于目标」,结束时 lo 即插入位置。注意 int 相加溢出与 1 未更新导致的死循环。

滑动窗口:right 右扩加入元素并更新状态,当窗口违规时 left 收缩直至重新合法,期间记录最优解;用数组/计数维护窗口内容。整体每个元素进出窗口各一次,时间复杂度 O(n)。

二分答案:当问题可转化为「给定阈值判断可行性」且可行性随阈值单调,就能二分阈值求最值(如最小化最大值、贪心可行性二分)。

工程实践

优先把模板掌握熟练(找边界 + 滑窗标准结构),代码要能跑通边界用例:空数组、单个元素、目标小于所有/大于所有。不要手工写易错的循环次数,用模板 + 单元自证。

高频追问
  • Q: 二分查不到时插入位置怎么确定?A: 用左界模板,循环结束时 lo 即是第一个大于等于目标的位置,即应插入下标。
  • Q: 滑动窗口何时适用?A: 子数组/子串满足单调可收缩条件、求最优连续段时;窗口随右界单调推进可一次扫描完成。
二分查找 双指针 滑动窗口 模板
E12.08
图与树遍历、递归、动态规划与贪心的适用边界专家P1
核心要点
  • DFS:递归/栈,适合路径、连通性、回溯;BFS:队列,适合最短步数、分层扩散、拓扑排序(入度法)
  • 递归三要素:终止条件、递推关系、返回值;注意栈深度(深树会栈溢出,可改迭代 + 显式栈)
  • 动态规划:最优子结构 + 重叠子问题;自底向上填表或自顶向下记忆化;经典背包、最长公共子序列、爬楼梯/斐波那契
  • 贪心:每一步取当前局部最优,仅在证明「局部最优 = 全局最优」(贪心选择性质)时可用,如区间调度、找零(特定币值)
  • 回溯:试探 + 撤销(状态回滚),解决排列组合、八皇后;本质是 DFS 加剪枝
详细解答

DFS vs BFS:DFS 实现简单但可能访问深而浪费,BFS 保证逐层找到最短步数(无权图)。Java 递归受线程栈限制(默认约 512KB),深度超过万级会 StackOverflow,大数据深树/图遍历需改显式栈或迭代。

动态规划要点:先定义 dp 数组含义 → 写状态转移 → 定初始值 → 定遍历顺序。贪心与之区别:贪心不保存子问题结果、只走一条路;DP 记录并依赖所有子问题;判断用贪心需验证「最优子结构 + 贪心选择」,反例(如分数背包 vs 0-1 背包)说明 DP 与贪心的边界。

记忆化优化:递归 + memo 缓存已解子问题(自顶向下)等价于自底向上填表,避免重复计算;二叉树遍历(前/中/后序递归、层序 BFS)是 DFS/BFS 最小模型。

工程实践

树/图题先判断用 BFS 还是 DFS:求最优步数、层序用 BFS;求存在性、路径枚举用 DFS + 回溯。DP 先写暴力递归再改 memo 是最稳妥的推导路线;能用贪心就尽量用贪心(复杂度最低),但要能给出正确性证明或至少反例不存在的理由。

高频追问
  • Q: DP 与贪心的本质区别?A: DP 依赖并复用所有子问题的最优解(探索全部状态),贪心只做无回溯的局部最优决策,仅在问题具有贪心选择性质时成立。
  • Q: 什么时候用记忆化(自顶向下)?A: 状态含大量不可达组合、只想求部分子问题时记忆化更省;状态全可达时自底向上递推更简单直接。
DFS BFS 动态规划 贪心