面试速查

Reference

Java 面试速查参考文档。覆盖 JVM、并发、集合、设计模式、Spring、Redis、消息队列、高可用、秒杀、SQL、数据库选型、HTTP 协议、请求全链路与单点登录(SSO)等,按左侧导航浏览。

持续更新

JVM 总览

JVM 是 Java 程序的运行环境,负责把字节码加载到内存中,通过解释执行或 JIT 编译运行,同时管理内存、线程、垃圾回收和运行时安全。

ClassLoader 运行时数据区 执行引擎 GC JNI

核心组成

类加载子系统

加载、验证、准备、解析、初始化 class 文件,把字节码变成 JVM 可以使用的运行时结构。

运行时数据区

包括堆、方法区、虚拟机栈、本地方法栈、程序计数器,是对象、线程栈帧和类元数据的主要存放区域。

执行引擎

负责执行字节码,包含解释器、JIT 编译器和垃圾回收器。

本地接口

通过 JNI 调用 C/C++ 等本地方法,常见于底层系统能力或高性能库。

内存模型

运行时内存结构

区域 线程共享 存放内容 常见异常
对象实例、数组,是 GC 管理的主要区域 OutOfMemoryError: Java heap space
方法区 / 元空间 类元数据、常量池、静态变量、JIT 编译后的代码等 OutOfMemoryError: Metaspace
虚拟机栈 方法调用的栈帧、局部变量表、操作数栈、返回地址 StackOverflowError
本地方法栈 Native 方法调用信息 StackOverflowErrorOutOfMemoryError
程序计数器 当前线程执行的字节码行号指示器 通常不会 OOM

堆内存分代

  • 新生代:大多数对象朝生夕死,通常分为 Eden、Survivor From、Survivor To。
  • 老年代:长期存活对象、大对象或经历多次 Minor GC 后晋升的对象。
  • 元空间:JDK 8 后替代永久代,使用本地内存,主要存储类元数据。

Java 内存模型 JMM

JMM 不是 JVM 运行时内存区域,而是 Java 并发语义规范,定义线程如何通过主内存和工作内存交互,解决可见性、有序性和原子性问题。

  • 原子性:基本读写通常具备原子性,复合操作如 i++ 不具备原子性。
  • 可见性:一个线程修改共享变量后,其他线程能否及时看到。volatile、锁、final 可提供可见性保障。
  • 有序性:编译器和 CPU 可能重排序,JMM 通过 happens-before 规则约束可观察结果。
面试注意:运行时数据区和 JMM 经常被混着问。回答时先区分:前者是 JVM 内存划分,后者是并发内存语义。

垃圾回收与调优

对象是否存活

  • 引用计数法:实现简单,但无法解决循环引用,JVM 主流 GC 不采用它作为核心判断方式。
  • 可达性分析:从 GC Roots 出发,能被引用链到达的对象是存活对象,否则可回收。

常见 GC Roots

  • 虚拟机栈中引用的对象,例如局部变量。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。
  • 被同步锁持有的对象。

垃圾回收算法

算法 特点 适用场景
标记-清除 先标记可回收对象再清除,会产生内存碎片 基础算法,老年代早期方案
复制算法 把存活对象复制到另一块区域,清理整块旧区域,效率高但浪费空间 新生代
标记-整理 标记后把存活对象向一端移动,解决碎片问题 老年代
分代收集 根据对象生命周期把堆分代,不同区域使用不同算法 现代 JVM 常见整体策略

常见垃圾回收器

  • Serial:单线程,简单稳定,适合客户端或小内存场景。
  • Parallel:吞吐量优先,适合批处理、计算任务。
  • CMS:低停顿老年代回收器,存在内存碎片问题,已逐步淘汰。
  • G1:面向服务端的低停顿收集器,把堆划分为多个 Region,可预测停顿时间。
  • ZGC / Shenandoah:面向超低停顿和大堆场景,停顿时间通常非常短。

调优思路

  1. 先明确目标:降低停顿、提升吞吐、减少 OOM,还是稳定响应时间。
  2. 收集证据:开启 GC 日志,结合监控、堆转储、线程栈分析。
  3. 判断问题类型:内存泄漏、对象创建过快、晋升失败、元空间不足、Full GC 频繁。
  4. 再调参数:堆大小、年轻代比例、GC 器选择、停顿目标、元空间上限。
  5. 压测验证:调优必须用真实流量模型或压测结果闭环。

常用参数

-Xms2g -Xmx2g
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc*:file=gc.log:time,uptime,level,tags
面试回答关键:GC 调优不是上来就改参数,而是先通过 GC 日志和监控确认问题,再结合业务目标选择收集器和参数,最后用压测验证。

类加载机制

类加载过程

  1. 加载:通过类的全限定名获取二进制字节流,转成方法区运行时数据结构,并在堆中生成 Class 对象。
  2. 验证:确保字节码符合 JVM 规范,不危害虚拟机安全。
  3. 准备:为类变量分配内存并设置默认初始值。
  4. 解析:把常量池中的符号引用转换为直接引用。
  5. 初始化:执行类构造器 <clinit>(),给静态变量赋代码中定义的值,执行静态代码块。
  6. 使用:程序正常访问类和对象。
  7. 卸载:类加载器可回收、类无实例、Class 对象无引用时,类才可能被卸载。

双亲委派模型

类加载器收到加载请求后,先委托父加载器尝试加载,父加载器无法加载时,子加载器才自己加载。

  • Bootstrap ClassLoader:加载 Java 核心类库。
  • Extension / Platform ClassLoader:加载扩展类库,JDK 9 后称 Platform ClassLoader。
  • Application ClassLoader:加载应用 classpath 下的类。
  • Custom ClassLoader:用户自定义类加载器,常见于热部署、插件化、隔离加载。

双亲委派的好处

  • 避免核心类被重复加载。
  • 保护 Java 核心 API,防止用户自定义 java.lang.String 等类替换核心类。
  • 保证类加载的一致性和安全性。

什么时候会打破双亲委派

  • JDBC、JNDI、SPI 等需要父加载器反向使用子加载器加载的实现类。
  • Tomcat 等容器需要隔离不同 Web 应用的类。
  • OSGi、插件化、热部署场景需要更灵活的类加载策略。

面试回答模板

问题:请你讲一下 JVM 内存结构

JVM 运行时数据区主要分为线程共享和线程私有两类。线程共享区域包括堆和方法区,堆主要存放对象实例,是 GC 的主要区域;方法区存放类元数据、常量、静态变量等。线程私有区域包括虚拟机栈、本地方法栈和程序计数器,虚拟机栈保存每次方法调用的栈帧,程序计数器记录当前线程执行位置。

问题:线上频繁 Full GC 怎么排查

我会先看 GC 日志和监控,确认 Full GC 的频率、耗时、触发原因以及各代内存变化。然后结合堆 dump 分析是否存在内存泄漏或大对象堆积;如果是对象创建过快,需要优化代码或缓存策略;如果是老年代空间不足或晋升失败,再考虑调整堆大小、年轻代比例或 GC 收集器参数。所有调整都要通过压测验证。

问题:双亲委派模型是什么,为什么需要它

双亲委派是类加载器加载类时先委托父加载器加载,父加载器加载不了再由自己加载。它的主要价值是保证核心类库的安全和唯一性,避免应用自己定义同名核心类破坏 JVM 行为,同时减少类的重复加载。

常见追问

  • JMM 和 JVM 内存结构有什么区别?JMM 是并发语义规范,关注可见性、有序性、原子性;JVM 内存结构是运行时数据区划分。
  • 为什么新生代适合复制算法?因为新生代对象大多很快死亡,存活对象少,复制成本低且能快速清理整块区域。
  • G1 为什么适合低停顿?G1 把堆拆成多个 Region,优先回收收益高的 Region,并可通过停顿目标控制回收节奏。
  • 类什么时候初始化?主动使用类时初始化,例如创建实例、访问静态变量、调用静态方法、反射调用、初始化子类前初始化父类。

多线程并发总览

并发编程要解决三类问题:如何创建和管理线程、如何保证共享数据安全、如何提升吞吐并控制资源消耗。生产环境通常用线程池而不是裸建线程,用锁或 CAS 保证一致性,用 JUC 工具类简化协作。

线程池 synchronized ReentrantLock volatile CAS AQS

线程状态

  • NEW → RUNNABLE → RUNNING / BLOCKED / WAITING / TIMED_WAITING → TERMINATED
  • sleep() 不释放锁;wait() 释放锁并进入等待队列。
  • yield() 让出 CPU,不保证立刻切换;join() 等待目标线程结束。

线程池

为什么用线程池

  • 降低频繁创建/销毁线程的开销。
  • 控制并发度,避免线程过多导致上下文切换和 OOM。
  • 统一管理任务提交、执行、拒绝和监控。

ThreadPoolExecutor 七大参数

参数含义
corePoolSize核心线程数,即使空闲也保留(除非 allowCoreThreadTimeOut)
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
unit存活时间单位
workQueue任务队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
threadFactory创建线程的工厂,可定制线程名便于排查
handler拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy

任务提交流程

  1. 当前线程数 < corePoolSize → 创建核心线程执行。
  2. 否则尝试入队;队列未满则排队。
  3. 队列满且线程数 < maximumPoolSize → 创建非核心线程。
  4. 否则执行拒绝策略。

常见线程池(Executors 工厂方法)

  • newFixedThreadPool:固定大小 + 无界队列,任务堆积时可能 OOM。
  • newCachedThreadPool:可扩容 + SynchronousQueue,高并发下线程数可能暴涨。
  • newSingleThreadExecutor:单线程顺序执行。
  • newScheduledThreadPool:定时/周期任务。
阿里规范建议:生产环境显式 new ThreadPoolExecutor,不用 Executors 快捷方法,以便明确队列边界和拒绝策略。

ForkJoinPool

分治任务专用,工作窃取(work-stealing)减少空闲;适合 CPU 密集型、可拆分的大任务,与 Stream 并行流底层相关。

锁机制

synchronized

  • 可修饰实例方法、静态方法、代码块;自动加锁解锁,不可中断、不可设置超时。
  • JDK 6 后锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(减少不必要的 OS 互斥)。
  • 保证原子性、可见性、有序性(通过 monitor 与内存屏障)。

ReentrantLock

  • 基于 AQS 实现,可重入;支持 lockInterruptibly()tryLock(timeout)
  • 可选公平锁(按申请顺序)或非公平锁(吞吐更高,默认)。
  • 需配合 Condition 实现多条件等待,类似多个 wait/notify 队列。

读写锁 ReadWriteLock

读多写少场景:读锁共享、写锁独占,提升读并发。StampedLock 支持乐观读,适合读远多于写。

volatile

  • 保证可见性、禁止部分重排序;不保证复合操作原子性(如 i++)。
  • 典型场景:状态标志位、双重检查锁中的实例引用发布。

锁对比

机制优点注意点
synchronized语法简单、JVM 优化成熟功能相对固定
ReentrantLock可中断、可超时、多 Condition必须 finally 中 unlock
volatile轻量、无阻塞不能替代锁做复合更新

死锁四要素与避免

  • 互斥、占有且等待、不可抢占、循环等待。
  • 避免:固定加锁顺序、tryLock 超时、缩小锁粒度、使用无锁结构。

CAS 与 AQS

CAS(Compare-And-Swap)

比较内存中的值与期望值,相等则更新为新值,是 CPU 原子指令实现的无锁乐观更新。

  • 优点:无阻塞、适合低竞争下的高性能计数、栈/队列头尾更新。
  • ABA 问题:值从 A→B→A,CAS 仍成功;可用 AtomicStampedReference 带版本号解决。
  • 自旋开销:竞争激烈时 CAS 反复失败,CPU 空转,此时锁可能更合适。

常用原子类

  • AtomicInteger / AtomicLong:基本类型原子操作。
  • AtomicReference:引用原子替换。
  • LongAdder:高并发计数,分段累加,比 AtomicLong 竞争小时更快。

AQS(AbstractQueuedSynchronizer)

JUC 锁与同步器的基础框架:state 表示同步状态,CLH 队列管理阻塞线程。

  • ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等均基于 AQS。
  • 独占模式 vs 共享模式;tryAcquire / tryRelease 由子类实现。

并发面试回答模板

问题:线程池参数怎么设

先看任务类型:CPU 密集型可接近 CPU 核数;IO 密集型可更大,如核数 × (1 + IO耗时/CPU耗时)。必须明确队列是否有界、拒绝策略和线程命名,并结合监控观察队列堆积和活跃线程数,避免无界队列导致 OOM。

问题:synchronized 和 ReentrantLock 区别

synchronized 是关键字,JVM 层面自动释放锁,有锁升级优化;ReentrantLock 是 API,基于 AQS,支持可中断、超时、公平锁和多 Condition。简单同步用 synchronized,需要高级控制用 ReentrantLock。

常见追问

  • ThreadLocal 内存泄漏?线程池复用线程时,若未 remove,Entry 的 key 弱引用被回收后 value 仍可能被强引用持有。
  • CAS 一定比锁快吗?不一定,高竞争下自旋浪费 CPU,应选锁或 LongAdder 等分段结构。

Lambda 表达式

函数式接口(仅一个抽象方法)的简洁实现,配合方法引用简化代码。

语法要点

  • (参数) -> 表达式(参数) -> { 语句; }
  • 类型可推断时可省略参数类型;单参数可省略括号。
  • 方法引用:类::静态方法实例::方法类::实例方法构造器::new
// 函数式接口
@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

Calculator add = (a, b) -> a + b;
List<String> names = Arrays.asList("Tom", "Jerry");
names.forEach(System.out::println);

内置函数式接口

接口方法用途
Predicate<T>test断言/过滤
Function<T,R>apply映射转换
Consumer<T>accept消费副作用
Supplier<T>get延迟供给

接口默认方法与静态方法

Java 8 允许接口有 defaultstatic 方法,便于 API 演进而不破坏实现类。

Stream API

对集合进行声明式、可链式、可并行的数据处理流水线,不存储元素,不修改源(除非明确可变操作)。

操作分类

  • 中间操作(惰性):filter、map、flatMap、distinct、sorted、peek、limit、skip
  • 终端操作(触发执行):forEach、collect、reduce、count、anyMatch、findFirst、min/max
List<User> result = users.stream()
    .filter(u -> u.getAge() >= 18)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .distinct()
    .collect(Collectors.toList());

// 分组与统计
Map<String, Long> byDept = users.stream()
    .collect(Collectors.groupingBy(User::getDept, Collectors.counting()));

并行流 parallelStream

  • 底层 ForkJoinPool.commonPool(),适合 CPU 密集型、无共享可变状态的任务。
  • 避免:IO 密集、小数据量、依赖顺序、涉及 synchronized 或非线程安全集合。
面试常问:Stream 与 Collection 区别 — Stream 是计算视图,单次消费;Collection 是数据结构,可多次遍历。

CompletableFuture

Java 8 增强的异步编程模型,支持组合、链式回调和异常处理,弥补 Future 只能阻塞 get 的不足。

常用 API

  • supplyAsync / runAsync:异步有返回值 / 无返回值
  • thenApply / thenAccept / thenRun:串行依赖上一步结果
  • thenCombine / allOf / anyOf:多任务组合
  • exceptionally / handle:异常恢复
  • orTimeout / completeOnTimeout(Java 9+):超时控制
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchOrder(), executor)
    .thenApply(order -> enrich(order))
    .exceptionally(ex -> { log.error("fail", ex); return "DEFAULT"; });

// 等待多个任务
CompletableFuture.allOf(f1, f2, f3).join();

与 Future / 回调对比

方式特点
Futureget 阻塞,难以链式组合
CompletableFuture非阻塞组合、显式线程池、异常传播可控
实践建议:始终传入自定义 Executor,避免默认 ForkJoinPool 与业务线程池混用;注意异常在链路上的传播,生产环境配合超时与熔断。

JDK 17 特性(LTS)

JDK 17 是长期支持版本,生产升级常选 11 → 17。以下为面试高频点。

语言与 API

  • 密封类 Sealed Classes:限制谁可以继承/实现,配合模式匹配更清晰建模。
  • 模式匹配 instanceof:判断类型同时绑定变量,减少强转样板代码。
  • Records:不可变数据载体,自动生成构造器、equals、hashCode、toString。
  • 文本块 Text Blocks:多行字符串 """ ... """,保留格式。
  • Switch 表达式增强-> 语法、yield、模式匹配 switch(预览演进中)。
public sealed interface Shape permits Circle, Rect {}
public record Point(int x, int y) {}

if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}

JVM 与库

  • 默认封装更强(强封装 JDK 内部 API),反射访问需 --add-opens
  • 移除 AOT/JIT 实验特性、Applet API、Security Manager 逐步废弃。
  • Foreign Function & Memory API(孵化/预览阶段演进)、Vector API 持续迭代。
  • macOS/AArch64 等平台支持成熟,ZGC、G1 等 GC 持续优化。
升级 17 时重点检查:反射/Agent、第三方框架版本、废弃 API(如 javax → jakarta 在 EE 场景)、构建插件与 CI 镜像。

JDK 21 特性(LTS)

JDK 21 是继 17 之后的 LTS,虚拟线程与若干预览特性进入主流面试题。

虚拟线程 Virtual Threads(Project Loom)

  • 轻量级线程,由 JVM 调度,适合高并发 IO 密集型(大量阻塞等待)。
  • Thread.startVirtualThread(runnable)Executors.newVirtualThreadPerTaskExecutor()
  • 不要池化虚拟线程;避免在虚拟线程中执行长时间 CPU 计算或 pin 住载体线程(如 synchronized 内阻塞,JDK 版本持续优化中)。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> fetchFromRemote(i)));
}

语言与其它重要特性

  • Record Patterns:解构 record,与 switch 模式匹配配合。
  • Pattern Matching for switch:正式版,类型模式 + null 处理更完整。
  • Sequenced Collections:统一首尾访问 API(getFirst、getLast、reversed)。
  • String Templates(预览):更安全、可扩展的字符串插值。
  • Generational ZGC:分代 ZGC 降低开销,改善吞吐与停顿。
  • Key Encapsulation Mechanism API:后量子密码相关标准 API。

17 → 21 选型简述

场景建议
稳定存量、框架未就绪优先 JDK 17 LTS
高并发 IO、微服务、新项目评估 JDK 21 + 虚拟线程
强依赖反射 hack 内部 API先解决模块化与 --add-opens 再升级

现代 Java 面试回答模板

问题:Stream 和 for 循环怎么选

简单遍历或性能敏感的热点路径可用 for;需要链式转换、分组、并行且逻辑声明式时用 Stream。Stream 有装箱和流水线开销,小集合不一定更快。

问题:CompletableFuture 如何避免线程池耗尽

为异步任务使用独立、有界线程池,区分 IO 与 CPU 任务;设置超时和拒绝策略;避免在 thenApply 里做阻塞 IO 占满默认池;大批量任务用分批或消息队列削峰。

问题:虚拟线程适合什么场景

适合大量短生命周期、以阻塞 IO 为主的任务,例如 HTTP 客户端调用、数据库访问。不适合 CPU 密集计算;与平台线程混用时注意线程本地变量和锁 pinning 问题。

常见追问

  • Lambda 捕获的变量为什么必须是 effectively final?避免并发语义下闭包内外状态不一致,简化实现。
  • Record 和 Lombok @Data 区别?Record 是语言级不可变语义,语义更清晰;Lombok 是编译期生成样板代码的普通类。
  • JDK 17 和 21 为什么都标 LTS?Oracle 每两年一个 LTS,17 和 21 都是长期支持节点,企业可按生态成熟度选择。

集合框架体系总览

java.util 集合框架以接口为核心,常用实现类在底层用数组、链表、哈希表或红黑树组织数据。选型时看:是否有序、是否允许重复/null、读写比例、是否并发。

Collection List Set Map Queue

继承关系(面试常画)

  • IterableCollectionList / Set / Queue
  • Map 独立接口,不继承 Collection;entrySet() 可当作 Set 视图操作

选型速查

需求推荐底层
随机访问、尾部追加多ArrayList动态数组
头尾插入删除多LinkedList双向链表
去重、无序、查快HashSetHashMap 的 key
去重且保持插入顺序LinkedHashSet哈希表 + 双向链表
去重且排序TreeSet红黑树
K-V、查改快HashMap数组 + 链表/红黑树
高并发读多写少ConcurrentHashMap分段/CAS + 桶锁
高并发读多、写极少CopyOnWriteArrayList写时复制数组

List 底层实现

ArrayList

  • 底层 Object[] elementData,默认初始容量 10(首次 add 时才真正分配)。
  • 扩容:容量不足时 grow(),新容量约为 old + (old >> 1),即约 1.5 倍,再 Arrays.copyOf 拷贝。
  • 随机访问 get(i) O(1);尾部 add 均摊 O(1);中间插入/删除需数组搬移 O(n)。
  • 非线程安全;fail-fast:迭代中结构修改抛 ConcurrentModificationException(modCount 检测)。

LinkedList

  • 双向链表,节点含 itemprevnext;同时实现 Deque,可作栈/队列。
  • 头尾插入删除 O(1);按索引访问需遍历 O(n)。
  • 内存开销大于 ArrayList(每个元素额外两个引用)。

Vector / Stack

  • Vector 方法 synchronized,线程安全但粒度粗、性能差,已被淘汰思路。
  • 需要线程安全 List 用 Collections.synchronizedList 或并发集合,而非 Vector。
面试结论:绝大多数场景用 ArrayList;只有频繁在头/中间插入删除且数据量大时才考虑 LinkedList。

Set 底层实现

Set 不允许重复元素,HashSetLinkedHashSet 内部本质是 HashMap(或 LinkedHashMap),元素存在 key 上,value 是固定占位对象 PRESENT

HashSet

  • 依赖 hashCode() 定位桶,equals() 判断相等;只重写 equals 不重写 hashCode 会破坏契约。
  • 无序、允许一个 null(JDK 8+ HashMap 树化后仍支持 null key 的单 null 限制在 HashMap 层)。

LinkedHashSet

  • 在 HashSet 基础上维护双向链表记录插入顺序(或访问顺序,构造参数 accessOrder)。
  • 略增内存,适合需要稳定遍历顺序的去重场景。

TreeSet

  • 底层 TreeMap,红黑树保证有序,增删查 O(log n)。
  • 元素需实现 Comparable 或构造时传入 Comparator;不允许 null(比较时会 NPE)。

Map 底层实现

HashMap(JDK 8+)

  • 结构:数组(桶)+ 链表;链表长度 ≥ 8 且数组长度 ≥ 64 时链表转红黑树;节点 ≤ 6 时树退化为链表。
  • 默认负载因子 0.75:平衡空间与时间,过大则链表/树变长,过小则频繁扩容。
  • 容量为 2 的幂:索引 (n - 1) & hash,等价取模且更快。
  • hash 扰动:高 16 位与低 16 位异或,减少低位相同导致的冲突。
  • put 流程:算索引 → 桶空则放头节点 → 否则比 key(先比 hash 再 equals)→ 冲突则尾插/树插 → 超阈值则 resize。
  • resize:容量翻倍,节点要么在原索引,要么在「原索引 + 旧容量」(利用 e.hash & oldCap 分流,JDK 8 优化)。
  • 非线程安全;多线程 put 可能死循环(JDK 7 头插扩容)或数据丢失,JDK 8 改为尾插但仍不安全。
// 扰动与寻址(概念)
hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
index = (table.length - 1) & hash;

LinkedHashMap

  • 继承 HashMap,额外维护双向链表:after 指针串联顺序。
  • accessOrder=false:插入顺序;true:访问顺序,可用于 LRU 缓存(配合 removeEldestEntry)。

TreeMap

  • 红黑树按 key 排序;ComparatorComparable 决定顺序。
  • 有序、O(log n),不适合 hash 均匀分布的高吞吐纯查找(HashMap 均摊 O(1) 更优)。

Hashtable

  • 方法级 synchronized,不允许 null key/value;遗留类,用 ConcurrentHashMap 替代。

HashMap vs Hashtable vs ConcurrentHashMap

HashMapHashtableConcurrentHashMap
线程安全是(粗粒度锁)
null允许 1 个 null key,多 null value不允许不允许
JDK 8 结构数组+链表+红黑树数组+链表数组+链表+红黑树
锁粒度整表桶头 synchronized / CAS

并发集合

ConcurrentHashMap(JDK 8+)

  • 取消 Segment 分段锁(JDK 7),改为对每个桶头节点 synchronized + CAS 插入空桶。
  • size 用 baseCount + CounterCell[] 分散计数,减少竞争。
  • 扩容支持多线程协助转移(ForwardingNode),复杂度高于 HashMap 单线程 resize。
  • get 一般无锁(volatile 读可见性);迭代弱一致性,不抛 CME。

CopyOnWriteArrayList / CopyOnWriteArraySet

  • 写时复制:修改时复制新数组再替换引用,读无锁、写成本高。
  • 适合读多写极少(白名单、监听器列表);写频繁或数据量大时内存与复制开销大。

其它

  • ConcurrentLinkedQueue:CAS 无界非阻塞队列。
  • BlockingQueue 家族:ArrayBlockingQueue(有界数组+一把锁)、LinkedBlockingQueue(可选有界链表)、PriorityBlockingQueue、DelayQueue、SynchronousQueue 等,用于线程池与生产者消费者。

集合面试回答模板

问题:HashMap 底层原理,为什么用红黑树

JDK 8 的 HashMap 是数组加链表,冲突严重时链表转红黑树,把最坏查找从 O(n) 降到 O(log n)。负载因子 0.75、容量 2 的幂是为了平衡扩容频率和哈希分布。put 时先算扰动后的 hash 定位桶,再比较 key,冲突则链表或树节点插入,超过阈值就扩容并 rehash。

问题:HashMap 线程不安全体现在哪

多线程同时 put 可能覆盖彼此的结果,扩容时也可能出现丢失或(JDK 7 头插)死循环。并发场景用 ConcurrentHashMap,不要用 Collections.synchronizedMap 包一层应付高并发(整表锁粒度大)。

问题:ArrayList 和 LinkedList 区别

ArrayList 基于动态数组,随机访问快、尾部添加均摊快,中间插入删除要搬移元素。LinkedList 是双向链表,头尾插入删除快,按索引访问要遍历。实际开发中 ArrayList 更常用,LinkedList 除非明确需要 Deque 语义或频繁头尾操作,否则优势不明显。

常见追问

  • hashCode 和 equals 契约?相等对象 hash 必须相同;hash 相同对象可不等,故还要 equals。
  • 为什么容量是 2 的幂?位运算取模、扩容时 rehash 可优化(JDK 8 高低位分流)。
  • Comparable 和 Comparator?前者内置于类,自然排序;后者外部策略,可多种排序规则。
  • fail-fast 和 fail-safe?前者迭代中检测 modCount 快速失败;后者(如 COW、CHM 迭代器)弱一致,不保证看到最新且一般不抛 CME。
  • 如何用 LinkedHashMap 做 LRU?构造 accessOrder=true,重写 removeEldestEntry 在超过容量时删除最久未访问条目。

SOLID 设计原则

面向对象设计的五条基本原则,目标是降低耦合、提高可维护性与可扩展性。

原则英文含义违反时的症状
S Single Responsibility 一个类只应有一个引起变化的原因(一个职责) 类臃肿、改一处牵动多处、难测试
O Open/Closed 对扩展开放,对修改关闭 新需求只能改老代码、if-else 膨胀
L Liskov Substitution 子类必须能替换父类且不改变程序正确性 子类重写后破坏父类契约、需 instanceof 特判
I Interface Segregation 客户端不应依赖它不需要的接口 「胖接口」、空实现、被迫实现无用方法
D Dependency Inversion 高层模块不依赖低层实现,都依赖抽象 业务层直接 new 具体类、难以替换与单测

面试记忆与示例

  • S:订单服务只处理订单逻辑,邮件通知拆到 NotificationService
  • O:促销规则用策略接口扩展,而不是在结算里不断加 if (双11)
  • L:正方形不应继承长方形(改一边会破坏另一边语义);子类不能加强前置、削弱后置条件。
  • I:Worker 拆成 WorkableEatable,机器人不必实现 eat()
  • D:Service 依赖 UserRepository 接口,由 Spring 注入 UserRepositoryImpl
SOLID 不是教条:微服务边界、框架约束、性能场景下可权衡;面试要说清「为什么」和「代价」。

创建型模式

关注对象的创建过程,将「如何 new」与业务逻辑解耦。

单例 Singleton

  • 保证全局唯一实例:配置中心、连接池管理器、Spring 单例 Bean(容器级)。
  • 写法:饿汉、懒汉、静态内部类、枚举(推荐,防反射与序列化破坏)。
  • 注意:多 ClassLoader、集群环境下「单例」语义不同;过度使用难测试。

工厂方法 Factory Method

  • 定义创建对象的接口,由子类决定实例化哪个具体类。
  • 示例:LoggerFactory.getLogger() 按配置返回不同 Logger 实现。

抽象工厂 Abstract Factory

  • 创建一族相关/依赖的产品(UI 主题:Button + TextField 成套)。
  • 与工厂方法:抽象工厂是「多产品族」,工厂方法是「单产品多实现」。

建造者 Builder

  • 分步构建复杂对象,链式调用,参数可选多。
  • 示例:Lombok @BuilderStringBuilder、HTTP Request 构建、MyBatis SqlSessionFactoryBuilder

原型 Prototype

  • 通过克隆已有对象创建新对象,避免重复昂贵初始化。
  • Java:Cloneable 浅拷贝;深拷贝需手动或序列化。Spring prototype scope 类似思想。

结构型模式

关注类与对象的组合,形成更大结构并保持灵活。

适配器 Adapter

  • 将不兼容接口转为客户端期望的接口。
  • 类适配器(继承)vs 对象适配器(组合,更常用)。
  • 示例:InputStreamReader 把字节流适配为字符流;对接第三方 SDK。

装饰器 Decorator

  • 动态给对象添加职责,比继承更灵活,可层层包装。
  • 示例:BufferedInputStream 包装 FileInputStreamCollections.synchronizedList
  • 与代理:装饰器侧重「增强功能」,代理侧重「控制访问」。

代理 Proxy

  • 为对象提供代理以控制访问、延迟加载、权限、日志、事务。
  • 静态代理:手写代理类;JDK 动态代理:基于接口;CGLIB:子类继承,可代理类。
  • Spring AOP、MyBatis Mapper、RPC 客户端均为典型应用。

外观 Facade

  • 为子系统提供统一简化入口,降低调用复杂度。
  • 示例:SLF4J 门面、封装多个微服务调用的聚合 Service。

其它(了解)

  • 桥接 Bridge:抽象与实现分离,如消息发送(邮件/短信)× 平台(Windows/Mac)。
  • 组合 Composite:树形结构统一对待叶子与容器,如菜单/文件夹。
  • 享元 Flyweight:共享细粒度对象,如字符串常量池、棋类游戏棋子状态共享。

行为型模式

关注对象之间的通信、职责分配与算法封装。

策略 Strategy

  • 封装一族算法,使它们可互换;消除条件分支。
  • 示例:Comparator、支付渠道(微信/支付宝)、路由规则、促销计算。

模板方法 Template Method

  • 父类定义算法骨架,子类重写某些步骤(钩子方法)。
  • 示例:AbstractList、Servlet service()、JUnit 测试生命周期、Spring JdbcTemplate

观察者 Observer

  • 一对多依赖:主题状态变化时通知所有观察者。
  • Java:Observable(已过时);Spring 事件 ApplicationEvent、MQ 发布订阅、Listener。

责任链 Chain of Responsibility

  • 请求沿处理者链传递,直到有人处理。
  • 示例:Servlet Filter 链、Netty Pipeline、审批流、日志级别链。

状态 State

  • 对象内部状态改变时改变其行为,用类代替大段 switch。
  • 示例:订单状态机(待支付→已支付→已发货)、TCP 连接状态。

命令 Command

  • 将请求封装为对象,支持撤销、排队、日志。
  • 示例:Runnable、事务命令、GUI 宏命令。

常用模式速查表

模式一句话JDK / 框架中的影子
单例全局唯一Runtime、Spring 单例 Bean
工厂封装创建Calendar.getInstance、LoggerFactory
建造者分步构建StringBuilder、Lombok Builder
适配器接口转换InputStreamReader
装饰器动态增强IO 包装流
代理控制访问Spring AOP、MyBatis Mapper
外观统一入口SLF4J
策略算法可换Comparator
模板方法固定流程JdbcTemplate
观察者发布订阅Spring Event、MQ
责任链链式处理Filter、Interceptor

设计面试回答模板

问题:说说你理解的 SOLID

单一职责让一个类只承担一类变化;开闭原则通过抽象和多态扩展而不是改旧代码;里氏替换保证子类可安全替换父类;接口隔离避免胖接口;依赖倒置让业务依赖接口,由 DI 注入实现。实际项目中我会结合 Spring 和策略/模板等模式落地,而不是为了原则而过度设计。

问题:代理模式在 Spring 里怎么体现

Spring AOP 默认对带接口的 Bean 用 JDK 动态代理,无接口时用 CGLIB 生成子类代理。切面逻辑(事务、日志、权限)织入目标方法前后,调用方注入的是代理对象,从而在不改业务代码的前提下增强功能,符合开闭原则。

问题:策略模式和 if-else 怎么选

分支少且稳定用 if-else 更直观;规则会频繁增加、需要独立测试或运行时切换时用策略模式,把每种算法封装成实现类,由上下文或工厂选择,符合开闭原则。支付渠道、计费规则是典型场景。

常见追问

  • 单例模式线程安全写法?推荐静态内部类或枚举;双重检查锁需 volatile 防指令重排。
  • 装饰器和代理区别?意图不同:装饰器扩展功能、可叠加;代理控制访问、常代表真实对象。
  • 工厂方法和抽象工厂区别?前者创建一个产品的一种实现;后者创建多个相关产品的一组实现。
  • 设计模式是否越多越好?否,过度设计增加复杂度;在变化点使用合适模式即可。

Spring Boot / Cloud 体系总览

Spring Boot 简化单体/微服务应用的启动与配置(自动配置、Starter、内嵌容器)。Spring Cloud 在 Boot 之上提供分布式能力:注册发现、配置中心、负载均衡、熔断限流、网关、链路追踪等。

Starter AutoConfiguration Nacos OpenFeign Gateway Sentinel

常见组件对照(面试版)

能力常见实现
注册与发现Nacos、Eureka、Consul
配置中心Nacos Config、Spring Cloud Config
负载均衡Spring Cloud LoadBalancer(替代 Ribbon)
服务调用OpenFeign、RestTemplate + LB
熔断限流Sentinel、Resilience4j(Hystrix 已停更)
API 网关Spring Cloud Gateway、Kong
链路追踪Micrometer Tracing、SkyWalking、Zipkin

自动配置原理

启动流程(精简)

  1. @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
  2. 运行 SpringApplication.run() → 创建 ApplicationContext(Servlet 环境多为 AnnotationConfigServletWebServerApplicationContext)
  3. 加载 application.yml/properties、环境 Profile、Bean 定义
  4. 执行各类 *AutoConfiguration,按条件注册 Bean
  5. 内嵌 Tomcat/Jetty/Undertow 启动 Web 容器

@EnableAutoConfiguration 做了什么

  • 通过 @Import(AutoConfigurationImportSelector.class) 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 2.7+ / 3.x;旧版为 spring.factories
  • 候选配置类经 @ConditionalOnXxx 过滤后生效

常用条件注解

注解含义
@ConditionalOnClassclasspath 存在某类
@ConditionalOnMissingBean容器中尚无该 Bean 时才注册默认实现
@ConditionalOnProperty配置项开关,如 xxx.enabled=true
@ConditionalOnWebApplicationWeb 应用环境

Starter 机制

  • Starter = 依赖聚合 + 自动配置模块,如 spring-boot-starter-webspring-boot-starter-data-redis
  • 自定义 Starter:autoconfigure 模块 + AutoConfiguration.imports + 配置属性类 @ConfigurationProperties
@Configuration
@ConditionalOnClass(RedisTemplate.class)
@EnableConfigurationProperties(RedisProperties.class)
public class MyRedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(...) { ... }
}
排查「Bean 未加载」:看是否被条件注解排除、是否被自定义 Bean 覆盖(OnMissingBean)、Profile 与包扫描路径是否正确。

循环依赖

什么是循环依赖

Bean A 依赖 B,B 又依赖 A(或更长环 A→B→C→A)。Spring 通过三级缓存解决单例 + 字段/Setter 注入场景下的循环依赖。

三级缓存(Singleton)

缓存名称内容
一级singletonObjects完全初始化好的单例 Bean
二级earlySingletonObjects早期暴露的 Bean(已实例化,可能未填完属性)
三级singletonFactoriesObjectFactory,用于生成早期引用(AOP 代理对象在此介入)

解决流程(A 依赖 B,B 依赖 A)

  1. 创建 A:实例化后放入三级缓存,继续属性注入发现需要 B
  2. 创建 B:实例化后放入三级缓存,注入 A 时从三级拿到 A 的早期引用(可能是代理)
  3. B 完成初始化,进入一级缓存
  4. A 拿到 B,完成初始化,进入一级缓存

无法解决的情况

  • 构造器注入循环依赖:实例化前就要 B,无法提前暴露半成品 → 启动失败
  • prototype 作用域:每次 new,不参与单例三级缓存
  • 多例 + 单例混合环、非 Spring 管理的 new 对象

实践建议

  • 优先重构:拆服务、事件驱动、@Lazy 延迟注入(治标)
  • 避免构造器环;必要时改用 Setter/字段注入并明确文档
  • Spring Boot 2.6+ 默认禁止部分循环依赖行为可配置:spring.main.allow-circular-references=true(不推荐长期依赖)

服务治理

微服务治理关注:服务如何被发现、如何可靠调用、如何限流熔断、如何统一配置与可观测。

注册与发现

  • 服务启动时向注册中心(Nacos/Eureka)注册 IP、端口、元数据、健康状态
  • 消费者从注册中心拉取/订阅服务列表,结合负载均衡选择实例
  • Nacos 同时支持 CP(Raft 持久实例)与 AP(临时实例)模式;Eureka 偏 AP(自我保护)

配置中心

  • 配置与代码分离,支持动态刷新(@RefreshScope + Spring Cloud Context)
  • 多环境 Namespace/Group、灰度发布、加密配置(Jasypt)

负载均衡与调用

  • @LoadBalanced RestTemplateWebClient + LoadBalancer 拦截
  • OpenFeign:声明式 HTTP 客户端,集成 LB、熔断、请求压缩;底层常为 HttpClient / OkHttp
  • 负载策略:轮询、随机、权重(Nacos 权重)、同 zone 优先等

熔断、限流、降级

  • Sentinel:QPS/并发线程数限流、熔断(慢调用比例、异常比例、异常数)、热点参数、系统自适应保护
  • 熔断状态:关闭 → 打开(快速失败)→ 半开(探测恢复)
  • 降级:返回默认值、缓存、友好提示,保证核心链路可用

其它治理能力

  • 分布式事务:Seata(AT/TCC/Saga)、最终一致性(消息表、可靠消息)
  • 链路追踪:TraceId 透传,分析慢调用与依赖拓扑
  • 服务契约:API 文档(OpenAPI)、版本兼容、消费者驱动契约(可选)

Spring Cloud Gateway

基于 Spring WebFlux + Netty 的响应式 API 网关,统一入口:路由、过滤、限流、鉴权、跨域、灰度。

核心概念

  • Route:路由 ID + 目标 URI + Predicate 集合 + Filter 集合
  • Predicate:断言匹配请求(Path、Method、Header、Cookie、时间窗口等)
  • Filter:Gateway Filter(路由级)与 Global Filter(全局),可改请求/响应

请求链路(简化)

  1. 客户端 → Gateway
  2. Global Filter 链(如负载均衡、限流、鉴权)
  3. 匹配 Route → 执行 Gateway Filter
  4. 通过 LoadBalancer 选择下游服务实例 → 转发
  5. 响应回传,可统一加 Header、日志、熔断
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

Gateway vs Zuul

GatewayZuul 1.x
模型WebFlux 非阻塞Servlet 阻塞
性能高并发下更易扩展线程池瓶颈明显
维护Spring 官方主推已进入维护态

典型能力实现

  • 统一鉴权:Global Filter 校验 JWT,下游信任网关或二次校验
  • 灰度发布:根据 Header/用户标签路由到金丝雀实例(结合 Nacos 元数据)
  • 限流:RequestRateLimiter + Redis;或与 Sentinel Gateway 适配器集成
  • 跨域:Cors WebFilter 或 Gateway 全局 CORS 配置

Spring 面试回答模板

问题:Spring Boot 自动配置原理

启动类上的 @EnableAutoConfiguration 会导入 AutoConfigurationImportSelector,读取 META-INF 下自动配置类列表,再结合 @ConditionalOnClass、OnMissingBean 等条件决定是否注册 Bean。Starter 把依赖和这些自动配置打包在一起,实现开箱即用;需要定制时用户自己声明同类型 Bean 即可覆盖默认实现。

问题:Spring 如何解决循环依赖

对单例 Bean 使用三级缓存:实例化后通过 ObjectFactory 提前暴露早期引用,让另一个 Bean 能先注入这个半成品,完成初始化后再放入一级缓存。若涉及 AOP,早期引用可能是代理对象。构造器注入的循环依赖无法这样解决,需要改设计或使用 @Lazy。

问题:Gateway 在架构中的作用

Gateway 作为南北向统一入口,负责路由转发到注册中心里的微服务实例,并在网关层做鉴权、限流、日志、跨域和灰度。它基于 WebFlux 非阻塞模型,适合高并发 API 场景,与 Nacos、LoadBalancer、Sentinel 等组件配合完成服务治理。

常见追问

  • @Bean 和 @Component 区别?@Component 是类路径扫描的通用组件;@Bean 是在 @Configuration 里用方法显式注册,适合第三方类。
  • Feign 和 RestTemplate?Feign 声明式、可集成熔断与契约;RestTemplate 更底层,需手写 URL 与 LB。
  • Nacos 和 Eureka?Nacos 兼具注册与配置;Eureka 仅注册发现,2.x 停更后新项目多选 Nacos/Consul。
  • 为什么 Gateway 用 WebFlux?少量线程处理大量 IO 等待,适合网关转发;注意阻塞调用需调度到 boundedElastic 避免阻塞事件循环。

Redis 总览

Redis 是基于内存的 KV 数据库,单线程执行命令(6.0+ 多线程主要处理网络 IO),支持丰富数据结构、持久化、主从复制、哨兵与 Cluster。常用于缓存、会话、排行榜、分布式锁、消息队列等。

单线程模型 RDB/AOF 主从 Sentinel Cluster

为什么快

  • 内存读写 + 高效数据结构
  • 单线程避免锁竞争(命令仍原子)
  • IO 多路复用(epoll)处理大量连接
  • 纯内存操作,持久化异步进行

数据结构

五大基础类型

类型底层(常见)典型场景常用命令
String SDS(简单动态字符串) 缓存对象、计数器、分布式锁、Session GET/SET、INCR、SETNX、EXPIRE
List quicklist(链表 + ziplist 压缩) 消息队列、最新列表(注意双端操作复杂度) LPUSH/RPOP、BLPOP、LRANGE
Hash ziplist 或 hashtable 对象字段存储(用户信息),小 Hash 省内存 HGET/HSET、HMGET、HINCRBY
Set intset 或 hashtable 标签、共同好友、去重、抽奖 SADD、SINTER、SUNION、SRANDMEMBER
ZSet(Sorted Set) skiplist + hashtable 排行榜、延迟队列、范围查询 ZADD、ZRANGE、ZRANK、ZINCRBY

高级 / 扩展类型

  • Bitmap:位操作,签到、活跃用户统计(BITCOUNT 需注意大数据耗时)
  • HyperLogLog:基数估算(UV),省内存,有误差
  • GEO:地理位置,底层 ZSet,附近的人
  • Stream:消息流,消费者组,类似轻量 MQ(持久化、ACK、Pending)
  • Pub/Sub:发布订阅,不持久化,订阅方离线会丢消息

键与过期

  • 过期策略:定期删除 + 惰性删除(访问时检查是否过期)
  • 内存淘汰(maxmemory-policy):noeviction、allkeys-lru、volatile-lru、allkeys-lfu 等
  • 生产建议:缓存必须设 TTL;热点 key 注意过期时间打散

持久化

方式特点
RDB快照,恢复快,可能丢最后一次快照后的数据
AOF记录写命令,数据更完整,文件大需 rewrite
混合AOF 重写时嵌入 RDB 前缀,兼顾恢复速度与完整性

集群与高可用

三种部署形态

模式作用说明
主从复制 读写分离、数据备份 主写从读;异步复制有延迟;从节点可做冷备
哨兵 Sentinel 自动故障转移 监控主从,主宕机选举新主;客户端连哨兵获取当前主地址
Cluster 水平扩展、分片 16384 个 hash slot,多主多从;无中心架构,Gossip 通信

Cluster 核心机制

  • key → CRC16(key) % 16384 → slot → 负责该 slot 的 master 节点
  • MOVED:slot 不在当前节点,客户端应更新槽位映射并重试
  • ASK:迁移中的临时重定向
  • Hash Tag:{userId}:profile{userId}:order 同 slot,保证事务/Multi 可用
  • 最少 3 主 3 从(官方建议),主宕机从节点晋升

复制与一致性

  • 主从异步复制:主成功写后返回客户端,再同步从库 → 主宕机可能丢少量已写未同步数据
  • wait 命令:可等待 N 个从库确认(慎用,影响延迟)
  • 脑裂:网络分区导致多主,需合理配置 min-slaves-to-write 等(版本与方案各异)
面试选型:数据量小、高可用 → 主从 + Sentinel;数据量大、需分片 → Cluster。云托管 Redis 通常封装了高可用与扩缩容。

缓存问题与解决方案

三大经典问题

问题现象方案
缓存穿透 查询不存在的数据,缓存和 DB 都没有,请求打到 DB 布隆过滤器;缓存空值(短 TTL);接口校验与限流
缓存击穿 热点 key 过期瞬间,大量请求同时查 DB 互斥锁(Redis SETNX)只放一个线程回源;逻辑过期(值带过期时间,异步刷新);热点 key 永不过期 + 后台更新
缓存雪崩 大量 key 同时过期或 Redis 宕机,DB 压力激增 TTL 加随机抖动;多级缓存;集群高可用;限流降级;Redis 持久化与备份

缓存与数据库一致性

  • Cache Aside(常用):读先缓存未命中再 DB 并回写;写先更新 DB 再删缓存(推荐删而非立刻更新,避免并发写乱序)
  • 延迟双删:删缓存 → 写 DB → 延迟再删一次,降低脏读窗口
  • 强一致场景:分布式锁、 Canal/MQ 订阅 binlog 异步删缓存、读主库
  • 接受最终一致性时,短 TTL + 主动失效是性价比最高的方案

热点 Key(Hot Key)

  • 本地缓存(Caffeine)挡一层;key 拆分(分片到多个 Redis key)
  • 读写分离、多副本;大促前预热;监控 QPS 异常 key

大 Key(Big Key)

  • 危害:阻塞单线程、迁移慢、内存不均、网卡打满
  • 治理:redis-cli --bigkeys、拆分 Hash/List、压缩、异步 UNLINK 删除

分布式锁(常考)

  • 基础:SET key uuid NX EX 30,解锁用 Lua 校验 uuid 再 DEL,防误删他人锁
  • 问题:主从切换可能导致锁丢失 → Redisson 看门狗续期、Redlock(有争议,生产需评估)
  • 强一致锁可考虑 ZooKeeper / etcd;Redis 锁适合允许极低概率重复的业务
// 互斥回源(击穿)伪代码
String v = redis.get(key);
if (v == null) {
    if (redis.setnx(lockKey, "1", "EX", 10)) {
        try {
            v = db.load(key);
            redis.setex(key, ttl, v);
        } finally {
            redis.del(lockKey);
        }
    } else {
        Thread.sleep(50);
        return get(key); // 重试或短暂降级
    }
}

Redis 面试回答模板

问题:Redis 为什么单线程还快

数据在内存,命令执行无磁盘随机 IO;单线程避免锁和上下文切换;使用 epoll 等多路复用处理大量连接。6.0 以后多线程主要分担网络读写,命令执行仍大体保持单线程语义。瓶颈通常在内存大小、网络带宽和大 key 慢操作,而不是 CPU 多核。

问题:缓存穿透、击穿、雪崩区别与解决

穿透是数据根本不存在,用布隆过滤器或空值缓存挡掉无效请求。击穿是单个热点 key 过期导致并发打 DB,用互斥锁或逻辑过期只让一个请求回源。雪崩是大量 key 同时失效或 Redis 不可用,用 TTL 随机化、集群高可用、限流降级和多级缓存来削峰。

问题:Redis Cluster 如何分片

集群把 16384 个 slot 分布在多个 master 上,客户端对 key 做 CRC16 取模得到 slot,再路由到对应节点。扩容时迁移 slot,期间用 ASK 重定向。需要多 key 事务时用 hash tag 保证相关 key 落在同一 slot。

常见追问

  • String 底层 SDS 好处?O(1) 取长度、二进制安全、减少扩容频率。
  • ZSet 为什么用跳表?实现简单,范围查询 O(log n),并发下比红黑树更易实现。
  • 先删缓存还是先写库?先写库再删缓存更常见;先删缓存再写库并发下易出现脏缓存。
  • Redis 和 Memcached?Redis 多数据结构、持久化、主从集群;Memcached 简单纯缓存、多线程。

消息队列总览

消息队列解耦、异步、削峰填谷。选型常看:吞吐、延迟、顺序、事务消息、运维生态、与现有技术栈契合度。

Topic Partition Consumer Group ACK 幂等

核心概念对照

概念RocketMQKafka
逻辑主题TopicTopic
分片单元Message Queue(Queue)Partition
消费组Consumer GroupConsumer Group
消费进度Broker 端 Offset(可集群消费)Broker 端 __consumer_offsets
顺序消息同一 Queue 有序同一 Partition 有序

RocketMQ vs Kafka 对比

维度RocketMQKafka
定位 业务消息中间件,阿里系电商/金融场景成熟 分布式流平台,日志采集、大数据管道、流计算首选
吞吐 高(万级~十万级 TPS 常见),略低于 Kafka 极限 极高(百万级 TPS 集群),顺序写磁盘 + Page Cache
延迟 毫秒级,支持定时/延迟消息级别丰富 毫秒级;延迟消息需自研或 Kafka 时间轮(版本特性)
消息模型 Topic + Queue;支持 Tag 过滤、SQL92 过滤 Topic + Partition;依赖 Consumer 或 Streams 处理
事务消息 原生半消息 + 回查(本地事务与消息一致性) 事务 API(0.11+),主要用于流式 exactly-once 生态
顺序消息 全局/分区顺序,Sharding Key 绑定 Queue 分区内有序,key 决定 partition
堆积能力 亿级堆积,依赖磁盘与消费速度 日志型存储,超长堆积与回溯是强项
消费模式 集群消费、广播消费 Consumer Group 负载均衡;独立 Consumer 可重复读
运维与生态 NameServer 轻量;国内文档与 Spring 集成多 ZK/KRaft、Connect、Streams、Flink/Spark 生态强
典型场景 订单、支付、库存、分布式事务、定时任务 日志、埋点、CDC、实时数仓、流处理

架构差异(简图理解)

  • RocketMQ:Producer → Broker(Master/Slave)← NameServer 路由;Consumer 拉取或长轮询 Push 效果
  • Kafka:Producer → Broker(Leader/Follower ISR)← Controller/KRaft;Consumer 拉取(poll),消费位点提交
选型口诀:业务交易、事务消息、延迟消息偏 RocketMQ;海量日志、流计算、超高吞吐偏 Kafka。很多公司两者并存。

消息可靠性

可靠性 = 消息不丢、不重复(或重复可接受)、可追踪。需从生产 → 存储 → 消费全链路设计。

生产者不丢

MQ手段
RocketMQ 同步发送 + 重试;事务消息;发送失败落库补偿任务
Kafka acks=all(ISR 全部确认);retriesenable.idempotence=true 防重
  • acks=0:不等待确认,可能丢
  • acks=1:Leader 写入即返回,Leader 宕机未同步可能丢
  • acks=all/-1:ISR 副本都确认,最可靠(需 min.insync.replicas 配合)

Broker 不丢

  • RocketMQ:同步刷盘 / 异步刷盘;同步复制 / 异步复制;Master-Slave 故障切换
  • Kafka:Partition 多副本 + ISR;replication.factor;unclean.leader.election 关闭可降低丢数据风险
  • 磁盘 RAID、监控磁盘使用率与副本滞后(Lag)

消费者不丢

  • 先执行业务再提交 offset(或 RocketMQ CONSUME_SUCCESS 后再确认)
  • 消费失败:重试队列 + 死信队列(DLQ)人工介入
  • 自动提交 offset 可能在处理中途崩溃导致「以为已消费」→ 手动提交更可控

RocketMQ 事务消息流程

  1. 发送 Half 消息(对消费者不可见)
  2. 执行本地事务
  3. Commit → 消息可见;Rollback → 丢弃
  4. Broker 回查本地事务状态(超时或未决时)

至少一次 / 至多一次 / 精确一次

语义含义实现要点
At most once可能丢,不重复先提交 offset 再消费
At least once不丢,可能重复先消费再提交 offset + 消费端幂等
Exactly once不丢不重Kafka 事务 + Streams;或业务层幂等表

幂等设计

MQ 普遍提供 至少一次(At Least Once) 投递,网络重试、Rebalance、消费超时会导致重复消费,业务必须幂等。

幂等层次

  • 生产幂等:Kafka enable.idempotence(PID + Sequence);业务侧用业务 ID 去重表
  • 消费幂等:核心,靠唯一键、状态机、数据库唯一约束

常见消费幂等方案

方案做法适用
唯一键 + DB 订单号/消息 ID 唯一索引,重复插入失败即已处理 创建类操作
幂等表 记录 messageId,处理前 INSERT,成功则标记;重复则跳过 通用
Redis SETNX messageId 作为 key,过期时间略大于重试窗口 高并发、允许极低 Redis 故障风险
状态机 仅允许 待支付→已支付,重复「已支付」消息直接返回成功 订单、工单流转
版本号/乐观锁 UPDATE ... WHERE version = ?,影响行数 0 则已处理或冲突 更新类操作
业务 Token 先申请一次性 Token,消费时校验并销毁 表单重复提交场景

设计要点

  • 消息体携带全局唯一 messageIdbizId,与业务主键对齐
  • 幂等记录与业务写放在同一本地事务,避免「记了幂等但业务失败」
  • 消费逻辑本身要可重入:外部调用(支付、发短信)也需幂等或先查状态
  • 重试次数与退避策略,最终进 DLQ,防止毒消息无限循环
// 幂等表伪代码
@Transactional
void onMessage(Message msg) {
    if (idempotentRepo.exists(msg.getMessageId())) {
        return; // 已处理
    }
    idempotentRepo.insert(msg.getMessageId()); // 唯一索引防并发
    orderService.createOrder(msg.getBody());
}
面试强调:MQ 保证不丢靠 ACK 与持久化;不重复靠「MQ 有限能力 + 业务幂等」组合,不要指望只调参数就 exactly-once。

MQ 面试回答模板

问题:RocketMQ 和 Kafka 怎么选

Kafka 擅长超高吞吐和流式生态,适合日志、埋点、实时数仓;RocketMQ 在业务消息、顺序、延迟、事务消息方面更贴近电商金融场景,和国内 Spring 集成成熟。若核心是订单支付一致性我会倾向 RocketMQ 事务消息;若是百万级日志管道选 Kafka。

问题:如何保证消息不丢

生产端同步发送并配置 acks=all 或 RocketMQ 同步刷盘与同步复制;Broker 多副本并监控 ISR/Lag;消费端先完成业务再提交 offset,失败走重试和死信。全链路还要有加锁、补偿任务和监控告警。

问题:如何处理重复消费

承认 MQ 通常是至少一次语义,在消费端做幂等:用 messageId 或业务唯一键配合数据库唯一索引、幂等表或 Redis 去重,结合状态机只推进合法状态。生产端 Kafka 可开幂等生产者减少重复写入。

常见追问

  • 消息堆积怎么办?扩容 Consumer、增加 Partition/Queue、优化消费逻辑、临时降级非核心消费、排查慢 SQL。
  • 顺序消息如何实现?同一 Sharding Key 进同一 Queue/Partition;消费单线程或分区锁。
  • 死信队列作用?重试耗尽后隔离毒消息,人工修复或丢弃,保护主队列。
  • Rebalance 影响?消费暂停、重复消费风险上升,应控制单次拉取量、优雅下线。

CAP / BASE 理论

CAP(分布式系统三选二)

  • C(Consistency)一致性:所有节点同一时刻读到相同数据
  • A(Availability)可用性:每个请求都能得到非错误响应(不保证最新)
  • P(Partition tolerance)分区容错:网络分区时系统仍能继续工作

分布式环境下网络分区不可避免,因此实际在 CPAP 之间权衡:

类型选择典型
CP牺牲可用性,保证强一致ZooKeeper、etcd、HBase、Redis 集群故障时部分不可用
AP牺牲强一致,保证可用Eureka、Cassandra、DNS

BASE(互联网大规模实践)

  • BA(Basically Available)基本可用:降级、限流、故障转移,核心功能可用
  • S(Soft state)软状态:允许中间状态存在一段时间
  • E(Eventually consistent)最终一致性:经过一段时间后达到一致
面试:CAP 是理论模型;生产用 BASE + 业务补偿实现「可接受的弱一致」,而不是追求处处强一致。

秒杀活动设计总览

秒杀(Seckill / Flash Sale)是在极短时间、极高并发下,对有限库存商品发起抢购。核心矛盾是: 读多写少、流量远超常态、库存绝不能超卖、用户体验还要可接受

业务特征

  • 流量尖峰:平时 QPS 几十,开场瞬间可到几万~几十万
  • 写热点:大量请求争抢同一 SKU 的少量库存
  • 失败是常态:库存 1000、请求 10 万,90%+ 用户应快速得到「已抢光」
  • 一致性要求高:超卖会引发生产事故;重复下单需幂等

设计总原则

  1. 流量层层递减:能挡在 DB 外的绝不进 DB(CDN → 网关 → 缓存 → MQ → DB)
  2. 库存扣减原子化:Redis Lua / 条件 UPDATE,禁止「先查后改」
  3. 异步削峰:秒杀成功只代表「获得购买资格」,订单异步创建
  4. 核心链路隔离:秒杀接口独立集群,与非核心功能舱壁隔离
  5. 可降级:评论、推荐、详情富文本等可关;下单链路保活

典型链路(一句话)

用户点击秒杀 → 网关限流 → Redis 校验资格并原子扣库存 → 发 MQ → 异步创建订单 → DB 最终扣减并幂等落库 → 用户轮询/推送「抢购结果」。

面试先讲「矛盾 + 分层思路」,再展开库存与 MQ;不要一上来堆组件名词。

流量分层与架构

分层职责

层级手段目标
客户端 按钮防抖、本地倒计时、活动未开始禁点、静态页 减少无效请求、提升体验
CDN / 静态 活动页 HTML、JS、图片走 CDN;页面数据接口少 扛住「看活动」的读流量
接入层 Nginx / Gateway 限流、IP 频控、黑名单、验证码 过滤机器人与恶意流量
应用层 活动信息 Redis 预热;登录校验;秒杀 API 独立部署 保护主站,热点逻辑隔离
缓存层 Redis 预减库存、资格校验、排队令牌 DB 前最后一道高并发屏障
消息层 MQ 异步下单、削峰填谷 把写压力从同步链路剥离
数据层 条件 UPDATE、唯一索引、订单幂等 最终一致、可对账、绝不超卖

活动页 vs 秒杀接口分离

  • 活动页:高读、可缓存,甚至提前生成静态 HTML + 本地倒计时
  • 秒杀接口:高写、短路径、少依赖,POST /seckill/{activityId} 独立域名或路径便于限流
  • 避免秒杀请求经过重 Filter 链、复杂鉴权 RPC、大 JSON 序列化

「排队 / 令牌」模式(可选)

超大规模场景可在 Redis 预减前增加排队层

  1. 请求进入 → 发放排队 Token(限流窗口内 N 个)
  2. 持 Token 才能调真正秒杀接口
  3. 无 Token 直接返回「排队中 / 活动太火爆」

本质是用漏桶把突发流量整形后再进 Redis,保护 Lua 脚本与 MQ 不被打满。

架构示意

用户 ──► CDN(静态页) ──► Gateway(限流/验证码)
              │
              └──► Seckill Service ──► Redis(Lua 扣库存)
                        │ 成功
                        └──► MQ ──► Order Consumer ──► MySQL(扣库存+建单)
                        │ 失败
                        └──► 快速返回「已抢光」

库存与防超卖

防超卖是秒杀的底线。多层防御,Redis 挡量,DB 做最终裁决。

Redis 预减库存(Lua 原子)

-- KEYS[1]=stock  ARGV[1]=userId
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then
  return 0  -- 无库存
end
redis.call('DECR', KEYS[1])
redis.call('SADD', 'seckill:users:'..KEYS[2], ARGV[1])  -- 一人一单
return 1
  • Lua 在 Redis 单线程执行,判断 + 扣减 + 记录用户 原子完成
  • 活动开始前将 DB 库存同步到 Redis;活动中以 Redis 为准快速失败
  • 活动结束后以 DB 对账,校准 Redis 与真实库存

数据库最终扣减

UPDATE seckill_stock
SET stock = stock - 1
WHERE activity_id = ? AND sku_id = ? AND stock > 0;
-- 影响行数 = 0 → 超卖防护生效,回滚 Redis 或标记补偿
  • 禁止:SELECT stockUPDATE stock = stock-1(并发下必超卖)
  • 可用乐观锁 version 字段,但高冲突下性能差,秒杀更常用条件 UPDATE

一人一单 / 防重复

层级实现
RedisSADD seckill:{actId}:users userId,已存在则拒绝
DBUNIQUE(activity_id, user_id) 唯一索引
接口幂等键 userId + activityId,重复请求返回同一结果

库存预热与一致性

  • 预热:活动开始前把库存、价格、规则写入 Redis,避免开场击穿 DB
  • 锁定:活动进行中 DB 库存行可标记为「秒杀锁定」,避免与普通订单混扣
  • 校准:定时任务对比 Redis 剩余与 DB;MQ 消费失败时 Redis 回滚或人工对账
  • 超卖兜底:DB 扣减失败 → 发补偿消息 / 取消 Redis 占位 / 通知用户「抢购失败」
Redis 扣减成功 ≠ 最终抢到。必须等订单创建成功才算成交;否则要有超时释放与对账机制。

异步下单与一致性

同步链路只做「资格判定 + 扣 Redis 库存 + 投递 MQ」,订单创建、支付、库存落库全部异步。

为什么用 MQ

  • 峰值写压力从 HTTP 线程转移到 Consumer,Consumer 按 DB 能力消费
  • HTTP 快速返回「排队处理中 / 抢购成功待确认」,避免长连接拖死 Tomcat 线程
  • 失败可重试、死信隔离,不丢单

消息体设计

{
  "messageId": "uuid",           // 全局幂等
  "activityId": 1001,
  "userId": 2002,
  "skuId": 3003,
  "seckillTime": 1710000000,
  "token": "redis扣减凭证"       // 可选,用于对账
}

消费端流程

  1. 幂等检查:messageIduserId+activityId 是否已处理
  2. DB 条件扣库存(最终防线)
  3. 创建订单(待支付),写入秒杀记录表
  4. ACK 消息;失败重试,超限进 DLQ

用户如何知道结果

方式说明
轮询GET /seckill/result/{activityId} 查 Redis/DB 状态
WebSocket / SSE订单创建后推送(体验好,实现成本高)
短信 / 站内信异步通知,适合支付截止提醒

与支付链路

  • 秒杀订单通常短支付窗口(如 15 分钟),超时自动关单并回滚库存
  • 关单逻辑:DB 加回库存 + Redis 可选同步 + 删除用户限购标记(若允许再次抢)
  • 支付成功走正常履约;取消/超时走补偿 Saga

分布式事务选型(秒杀场景)

一般不引入 2PC/TCC,采用可靠消息 + 最终一致 + 幂等 + 对账

  • Redis 扣减与 MQ 发送:本地事务表或 RocketMQ 事务消息保证「扣了必发 / 发了可对账」
  • Consumer 失败:重试 + DLQ + 人工/脚本补偿
  • 定时对账:Redis 计数 vs 已支付订单 vs DB 库存

风控、缓存与容错

恶意流量防护

  • 登录 + 实名:秒杀前必须登录,减少匿名刷接口
  • 验证码 / 滑块:开场前或点击秒杀时触发,挡脚本
  • 限流维度:IP、用户 ID、设备指纹、接口级 QPS
  • 黑名单:异常 IP/账号实时封禁(Redis Set)
  • 链接签名:秒杀 URL 带时效 token,防直接刷 API

缓存问题

问题秒杀场景对策
缓存击穿活动信息热点 key 过期预热 + 逻辑过期(旧值 + 异步刷新)
缓存穿透恶意查不存在 activityId布隆过滤器 + 空值短缓存
缓存雪崩大量 key 同时过期过期时间加随机抖动;多级缓存
热点 key单 SKU 库存 key 打满单 Redis 节点本地缓存 + Redis 分片;库存拆分为多 sub-key(高级)

降级与隔离

  • 秒杀期间关闭:商品推荐、评价列表、非必要 RPC
  • Sentinel / Hystrix:秒杀服务独立线程池,下游超时快速失败
  • 熔断:支付/库存服务异常时停止新秒杀入口,保护已有订单
  • 静态降级页:「活动太火爆,请稍后再试」

监控与压测

  • 核心指标:秒杀 QPS、Redis 扣减成功率、MQ 堆积、订单创建 RT、超卖告警(DB 扣减失败率)
  • 全链路压测:模拟 10~100 倍峰值,验证限流阈值与 Consumer 消费能力
  • 预案:扩容 Consumer、降级开关、活动延期/熔断入口

秒杀面试回答模板

问题:如何设计一个秒杀系统?

秒杀的核心是瞬时高并发抢有限库存,我会从前到后层层过滤流量:CDN 和静态活动页、网关限流和验证码、活动数据 Redis 预热。扣库存不在 DB 同步做,而是用 Redis Lua 原子预减,并校验一人一单;成功后再发 MQ 异步创建订单,数据库用 UPDATE ... WHERE stock > 0 做最终扣减,配合唯一索引和消息幂等防超卖和重复单。秒杀服务独立部署,非核心功能降级,全程监控 MQ 堆积和 DB 扣减失败率,并做全链路压测。

问题:Redis 预减成功,DB 扣减失败怎么办?

说明 Redis 与 DB 出现不一致,需要补偿:Consumer 发现 DB 影响行数为 0 时,不创建订单,记录失败并可选回滚 Redis 库存或标记对账;通知用户抢购失败。平时用定时任务对比 Redis 剩余库存、已创建订单数与 DB 真实库存。设计上 DB 是最终权威,Redis 是高性能前置过滤器。

问题:为什么秒杀要用 MQ,不能直接同步下单?

同步下单会把订单创建、库存写库、可能还有优惠券校验都放在 HTTP 链路里,DB 连接和线程会被峰值打满,RT 飙升导致超时重试,反而放大流量。MQ 把写操作异步化,HTTP 只做轻量资格判定并快速返回,Consumer 按数据库承受能力平滑消费,配合幂等和死信保证不丢单。

问题:如何防止超卖和一人多单?

超卖:Redis Lua 原子判断扣减 + DB 条件 UPDATE 双重保障,禁止先 select 再 update。一人多单:Redis Set 记录已抢用户 + 数据库 unique(activity_id, user_id) + 接口幂等。消息消费也要幂等,防止 MQ 重复投递产生多单。

常见追问

  • 库存存在 Redis 丢了怎么办?以 DB 为准重建;活动开始前预热;Redis 集群持久化与高可用;关键活动可 Redis + 本地缓存双写。
  • 热点 key 问题?单 key 打满单分片;库存拆分多个 sub-stock;或请求排队令牌削峰。
  • 和一般高并发下单区别?秒杀库存极少、失败率极高、可接受异步确认;电商常态下单更强调同步体验和复杂促销规则。
  • 秒杀需要分布式锁吗?扣库存优先 Lua/条件 SQL,锁粒度大、RT 高;锁更适合定时任务或补偿任务互斥。

分布式事务

跨服务、跨库、跨 MQ 的一致性。强一致成本高,互联网多采用最终一致性

方案对比

方案一致性特点
2PC / XA 强一致 协调者两阶段提交;阻塞、性能差,微服务少用
TCC 最终/准强 Try-Confirm-Cancel,业务侵入大,需空回滚、防悬挂
本地消息表 最终一致 业务与消息同库事务,定时扫描发 MQ
事务消息(RocketMQ) 最终一致 半消息 + 本地事务 + 回查
Saga 最终一致 长事务拆本地事务 + 补偿;编排式或 choreography
Seata AT 弱强一致 自动生成回滚 SQL,对业务透明,依赖 undo_log

选型建议

  • 短链路、强一致要求:Seata AT / TCC(量不大时)
  • 订单+库存+支付:Saga 补偿或可靠消息 + 幂等
  • 原则:能避免分布式事务就避免(合并服务、单库);必须用时优先异步最终一致

分布式锁

在分布式环境下互斥执行:库存扣减、定时任务单实例、防重复提交。

实现对比

实现优点缺点
Redis(SET NX EX) 性能高、实现简单 主从切换可能丢锁;需 Lua 安全解锁;Redisson 看门狗续期
ZooKeeper / etcd 强一致、临时顺序节点公平 性能低于 Redis;运维成本高
数据库唯一索引 无额外组件 性能差、锁表风险
Redisson 可重入、看门狗、读写锁、联锁 依赖 Redis 集群可用性

正确使用要点

  • 锁 value 用 UUID,解锁 Lua 脚本校验「只删自己的锁」
  • 过期时间 > 业务最大耗时,或看门狗自动续期
  • 锁粒度尽量小;能不用锁则用 DB 乐观锁 / 原子 SQL
  • Redlock 有争议,金融强一致场景慎选 Redis 锁

限流

保护系统不被突发流量压垮,保证核心链路可用。可在网关、应用、Redis 多层实施。

常见算法

算法特点场景
固定窗口实现简单,边界处可能双倍突发粗粒度限流
滑动窗口更平滑,统计更准接口 QPS
漏桶(Leaky Bucket)恒定流出速率,削峰下游处理能力固定
令牌桶(Token Bucket)允许一定突发,平均速率受限Guava RateLimiter、Sentinel

实现层级

  • 网关:Spring Cloud Gateway RequestRateLimiter、Nginx limit_req
  • 框架:Sentinel 按资源名 QPS/线程数、热点参数限流
  • 分布式:Redis + Lua 全局限流;按用户/IP/接口维度

与降级区别

  • 限流:超过阈值直接拒绝或排队,保护系统
  • 降级:关闭非核心功能,返回兜底数据,保证主流程

熔断与降级

防止故障依赖拖垮整个系统(雪崩)。熔断快速失败;降级提供备选方案。

熔断器状态机(Hystrix / Sentinel / Resilience4j)

  1. Closed:正常调用,统计错误率/慢调用比例
  2. Open:超过阈值则熔断,直接失败或走 fallback,经过休眠时间后进入半开
  3. Half-Open:放行少量探测请求,成功则关闭熔断,失败则继续打开

触发条件(常见)

  • 错误比例 > 50%(滑动窗口内)
  • 慢调用比例 > 阈值(RT 超过设定 ms)
  • 异常数、并发线程数超限(Sentinel)

实践组合

手段作用
超时避免线程阻塞堆积
重试幂等接口有限次重试 + 退避
熔断依赖故障时快速失败
降级返回缓存/默认值,关闭推荐位
隔离线程池/信号量隔离,Bulkhead 舱壁模式
限流入口削峰
Sentinel 侧重限流+熔断规则可视化;Hystrix 已停更,新项目多用 Sentinel 或 Resilience4j。Feign 可集成 fallbackFactory 做降级。

高可用面试回答模板

问题:如何设计一个秒杀系统

详见 秒杀面试模板。简要:流量分层过滤 → Redis Lua 预减库存 → MQ 异步下单 → DB 条件扣减 + 幂等防超卖,秒杀链路独立部署并配合限流降级。

问题:CAP 怎么理解,和 BASE 关系

分区容错在分布式里必须考虑,所以实际是在一致性和可用性之间权衡。CP 如 ZooKeeper 选举期间可能不可用;AP 如 Eureka 保证注册服务可读但可能读到旧列表。BASE 是 AP 的延伸,通过基本可用和最终一致性满足互联网业务,配合消息、补偿和幂等落地。

问题:分布式事务怎么做

优先避免:合并服务或单库。必须跨服务时,强一致场景可用 Seata AT 或 TCC;大部分业务用可靠消息、事务消息或 Saga 补偿实现最终一致,关键是幂等和可对账。2PC 性能差,微服务里较少用。

问题:限流和熔断区别

限流是控制进入系统的请求速率,防止过载;熔断是下游故障时切断调用链,快速失败防止线程拖死。通常入口限流 + 调用下游超时重试 + 熔断降级组合使用,配合舱壁隔离线程池。

常见追问

  • 雪崩效应?依赖故障 → 线程阻塞 → 资源耗尽 → 级联失败;用超时、熔断、隔离、限流打破链条。
  • TCC 空回滚?Try 未执行却收到 Cancel,需识别并直接成功;悬挂是 Cancel 先于 Try,需防重。
  • 令牌桶 vs 漏桶?令牌桶允许突发;漏桶输出恒定,更适合平滑写下游。

SQL 调优总览

调优目标:更快返回(降低 RT)或更少资源(CPU/IO/扫描行数/Shuffle 量)。先定位瓶颈,再改 SQL、索引或架构。

通用排查步骤

  1. 看慢查询日志 / APM / 执行计划(EXPLAIN / EXPLAIN ANALYZE
  2. 确认是否全表扫描、错误索引、回表过多、Join 顺序差
  3. 检查数据量、统计信息是否过期(ANALYZE TABLE
  4. 改写 SQL、加/改索引、拆查询、预聚合、缓存
  5. 压测验证,对比 rows examined、执行时间、成本

MySQL EXPLAIN 关键列

关注点
typesystem > const > eq_ref > ref > range > index > ALL(尽量避免 ALL)
key实际使用的索引
rows预估扫描行数,越小越好
ExtraUsing filesort、Using temporary 需警惕

索引基本原则

  • 最左前缀:联合索引 (a,b,c) 可用于 a、ab、abc 条件
  • 区分度高的列放前面;避免对索引列函数/隐式类型转换(WHERE YEAR(dt)=2024 坏,范围改区间好)
  • 覆盖索引减少回表;写多读少表慎加索引

谓词下推(Predicate Pushdown)

WHEREHAVING 等过滤条件尽可能下推到数据源附近先执行,减少向上层传递的数据量,降低网络、内存与 CPU 成本。

关系型数据库(MySQL / PostgreSQL)

  • 优化器自动下推:能走索引的条件在存储引擎层过滤
  • 子查询改写:将过滤写在子查询内部,避免先全量再过滤
  • 视图/派生表:外层 WHERE 可能合并进内层(MySQL 5.7+ 条件下推优化)
-- 不推荐:外层才过滤
SELECT * FROM (
  SELECT * FROM orders WHERE status IN (1,2,3)
) t WHERE t.created_at >= '2024-01-01';

-- 推荐:条件下推至最内层
SELECT id, amount, created_at
FROM orders
WHERE status IN (1,2,3)
  AND created_at >= '2024-01-01'
  AND user_id = ?;

数仓 / 大数据(Hive、Spark SQL、Presto)

  • 分区裁剪:WHERE 带分区列(dt='2024-05-01')只读对应目录
  • 列裁剪:只 SELECT 需要的列,减少 ORC/Parquet 读取
  • 谓词下推至存储:文件级跳过 Stripe/Row Group(谓词 + 列存统计信息 min/max)
  • 避免在最外层才过滤几亿行明细;ETL 尽早 Filter

无法在 SQL 层下推时注意

  • 对计算列过滤:WHERE UPPER(name)='ABC' → 考虑生成列 + 索引或冗余列
  • 跨库 Join:先在各库内过滤再联邦 Join,减少中间结果
  • ORM 生成 SQL 丢失条件:检查 N+1、全表 load 再内存 filter
面试表述:谓词下推 =「尽早过滤、少传数据」;OLTP 靠索引+优化器,OLAP 靠分区+列存+下推至文件块。

Join 优化

Join 类型与代价

类型说明注意
INNER JOIN只保留匹配行最常用的优化对象
LEFT JOIN保留左表全部右表条件放 ON 还是 WHERE 结果不同
STRAIGHT_JOIN强制 MySQL 连接顺序仅在确认优化器选错时用

MySQL Join 算法(NLJ / BNL / Hash)

  • Nested Loop Join:驱动表逐行嵌套被驱动表;被驱动表需有索引(eq_ref/ref)
  • Block Nested Loop:无索引时缓存驱动表块,仍较慢
  • Hash Join(8.0.18+):等值连接大表无合适索引时,内存建哈希表

优化要点

  1. 小表驱动大表(传统 NLJ):减少外层循环次数;8.0 优化器常自动选驱动表
  2. 被驱动表 Join 列建索引:避免对被驱动表全表扫描
  3. 先过滤再 Join:各表 WHERE 后再 Join,缩小中间结果(谓词下推)
  4. 避免 Join 列类型/字符集不一致:导致无法走索引
  5. 控制 Join 表数量:超 3~4 张考虑宽表、物化中间表或业务拆分
  6. 数仓大表 Join:Map Join(广播小表)、Bucket Join(分桶对齐)、Skew Join 处理数据倾斜
-- LEFT JOIN 条件位置
-- 过滤右表且不要保留右表无匹配:条件放 WHERE
SELECT a.* FROM users a
LEFT JOIN orders b ON a.id = b.user_id
WHERE b.created_at >= '2024-01-01';  -- 无订单用户会被滤掉,语义等同 INNER

-- 保留左表全部:右表过滤放 ON
SELECT a.* FROM users a
LEFT JOIN orders b ON a.id = b.user_id AND b.created_at >= '2024-01-01';

子查询 vs Join

  • IN (SELECT ...) 有时可改 EXISTS 或 Join,便于走索引
  • 避免相关子查询逐行执行;改 Join 或窗口函数(MySQL 8.0)

报表查询提效与降本

报表多为读多写少、聚合重、时间范围大,目标:少扫明细、少重复计算、可缓存。

架构层手段

手段作用
读写分离报表走从库,隔离 OLTP
汇总表 / 宽表按天/店铺预聚合,查询只扫汇总行
物化视图定时刷新中间结果(MySQL 可定时任务写汇总表)
OLAP 引擎ClickHouse、Doris、StarRocks 做多维分析
缓存Redis 缓存热点报表;结果带版本与 TTL
异步导出超大报表走 MQ + 离线生成,避免拖垮在线库

SQL 层降本

  • 分区表:按日/月分区,查询必带分区键,避免扫描全部分区
  • 只查必要列:禁止 SELECT * 扫宽表
  • 分页优化:深分页 LIMIT 100000,20 慢 → 用上次最大 ID 游标:WHERE id > ? ORDER BY id LIMIT 20
  • 聚合下推:在数仓层 GROUP BY,应用层不再拉明细聚合
  • 近似计算:UV 用 HyperLogLog、采样统计降低精确度换速度
  • 冷热分离:历史数据归档到对象存储 + 外表查询

降低计算成本(云/集群)

  • 减少全表 COUNT(*):用统计表或估算(SHOW TABLE STATUS
  • 合并重复报表任务:同一汇总表多报表复用
  • 调度错峰:T+1 凌晨批处理,避免与高峰 OLTP 抢资源
  • Spark/Hive:控制 Shuffle 分区数、广播小维表、过滤前置、缓存复用中间 Dataset
  • 监控:扫描字节数、Spill 磁盘、任务时长,设资源配额

反模式(报表常见)

  • 在线库直接跑大范围 JOIN + GROUP BY + 排序
  • 实时库做复杂多维下钻无索引维度
  • 每次请求全量重算,无增量更新

SQL 面试回答模板

问题:慢 SQL 怎么排查和优化

先用慢日志和 EXPLAIN 看是否全表扫描、索引未命中、filesort 或临时表。然后检查 WHERE 是否命中联合索引最左前缀、是否有隐式转换。改写为先过滤再 Join、避免 SELECT *,必要时加覆盖索引或汇总表。数仓场景还要做分区裁剪和谓词下推,减少扫描数据量。

问题:什么是谓词下推

把过滤条件尽量推到离数据源最近的一层执行,在读文件或走索引时就丢掉不符合条件的行,减少后续 Join 和聚合的数据量。在 MySQL 里体现为优化器下推和子查询内写清条件;在 Hive/Spark 里还配合分区裁剪和列式存储的 Row Group 跳过。

问题:Join 很慢怎么优化

保证 Join 列有索引且类型一致;各表先 WHERE 缩小结果集;控制 Join 表数量。MySQL 让小结果集做驱动表,被驱动表走 ref/eq_ref。大表等值 Join 可看 8.0 Hash Join。数仓里对小维表用 Map Join,解决倾斜用随机前缀或单独处理热点 key。

问题:报表查询如何不影响线上又降成本

报表走从库或 OLAP,用按日汇总表和定时任务预计算,查询只扫聚合结果。SQL 上带分区键、避免深分页、异步导出大结果。批处理错峰,监控扫描量和 Shuffle,能近似就近似,历史数据归档。

常见追问

  • 覆盖索引?索引包含查询所需全部列,避免回表。
  • 索引失效场景?对列运算、不等于、前导模糊 %abc、OR 未全覆盖、类型转换。
  • filesort 一定慢?内存 sort buffer 够用可接受;数据量大考虑索引有序或降序需求改设计。
  • COUNT(1) vs COUNT(*)?InnoDB 下优化器处理相近,性能差异可忽略,看语义即可。

数据库选型总览

先分清 workload:OLTP(高并发短事务、强一致)vs OLAP(大批量扫描、聚合分析)vs HTAP(两者都要,有取舍)。

数据库定位一句话
MySQLOLTP互联网业务库首选,生态成熟
PostgreSQLOLTP + 复杂 SQL功能最全的开源关系库,扩展强
ClickHouseOLAP列存分析引擎,聚合极速
Hologres实时数仓 / HTAP云原生实时分析,兼容 PG 协议,离线与实时一体

OLTP vs OLAP

维度OLTPOLAP
典型操作INSERT/UPDATE 单行、点查大范围扫描、GROUP BY、JOIN 大表
数据模型行存、范式设计列存、宽表/星型模型
一致性强一致 ACID常最终一致,批量导入
扩展主从、分库分表分布式分片、MPP

MySQL / PostgreSQL / ClickHouse / Hologres 对比

维度 MySQL PostgreSQL ClickHouse Hologres
类型 关系型 OLTP 关系型 OLTP(可轻量 OLAP) 列存 OLAP 实时数仓(行列混合,HTAP 倾向)
存储引擎 InnoDB 行存为主 Heap 行存,扩展列存(cstore 等) MergeTree 系列列存 行列共存,Segment 存储,支持实时写入
事务 ACID 完整支持(InnoDB) 完整支持,MVCC 弱/无传统事务,批量导入为主 支持主键表事务语义;分析表偏批量
并发写入 高,行级锁 高,MVCC 读写不阻塞 批量写入优,高频单行更新弱 高吞吐实时写入(Flink 导入等)
点查 / 主键 极强,B+ 树索引 强,多种索引(B-tree、GIN、GiST) 主键查询可用但非强项 主键表支持高 QPS 点查 + 分析
复杂分析 弱,大表聚合慢 中等,窗口函数、CTE 强 极强,向量化执行 强,MPP 并行,面向 PB 级
JOIN 大表 吃力 中等 看引擎与内存,宽表更好 优化器强,适合数仓星型模型
更新删除 频繁 UPDATE/DELETE 同左 不推荐频繁改;Collapsing/Replacing 引擎 支持 UPSERT、增量更新,仍偏分析场景
SQL 兼容 MySQL 方言 标准 SQL 程度高 类 SQL,函数/语法有差异 PostgreSQL 协议兼容
扩展性 主从、中间件分片(ShardingSphere) 流复制、Citus 分布式 分片集群,本地表 + Distributed 表 云原生弹性扩缩容,存算分离
生态 Java 最熟、云 RDS 普及 GIS、JSON、时序(Timescale)插件 日志/监控/BI 集成广 阿里云、Flink/MaxCompute/DataWorks 一体
部署 自建/RDS 成熟 自建/RDS/云托管 自建/云 CK 服务 主要为阿里云托管
成本模型 按实例,OLTP 性价比好 同左,复杂查询需调优 存储压缩高,分析 QPS 成本低 按 CU/存储,实时数仓综合计费

各库核心特点(面试记忆)

MySQL

InnoDB B+ 树、redo/undo、主从复制;适合订单、用户、交易;分库分表成熟。

PostgreSQL

MVCC 无回滚段读阻塞、丰富索引与类型;适合复杂查询、地理、JSON、金融合规场景。

ClickHouse

列存 + 向量化 + 数据压缩;MergeTree 后台合并;适合日志分析、大屏、用户行为聚合。

Hologres

实时写入 + 交互式查询;与 Flink 双流、MaxCompute 离线打通;Serving 层点查 + 分析一体。

场景与典型架构

选型建议

场景推荐说明
电商订单、支付、账户MySQL / PG强事务、行级锁、成熟 ORM
复杂报表、存储过程、GISPostgreSQLSQL 能力强、扩展多
APP 埋点、日志、监控指标ClickHouse高压缩、聚合快、成本低
实时大屏、即席分析、湖仓一体(阿里云)HologresFlink 实时入仓 + 多维分析 + 在线 Serving
既要交易又要实时分析MySQL + CK/HologresCDC(Canal/Debezium)同步到分析库,勿硬扛 OLTP

Lambda / 湖仓数据流(常见)

  1. 业务库 MySQL → Binlog → Flink/Canal
  2. 实时层写入 Hologres 或 Kafka → ClickHouse
  3. 离线层 MaxCompute/Hive T+1 汇总,再回灌 Hologres 做融合查询
  4. BI / 报表 / API 查询走 OLAP,保护 OLTP

ClickHouse vs Hologres(常对比)

维度ClickHouseHologres
优势极致单表聚合、开源可控、压缩比高实时写入低延迟、PG 生态、阿里云数据栈集成、主键点查
劣势UPDATE 弱、分布式 Join 需建模厂商绑定、成本依赖云定价
更适合日志/指标、自助分析、成本敏感海量扫描实时数仓、Flink 实时链路、需要 PG 兼容的 Serving

MySQL vs PostgreSQL(常对比)

  • MySQL:简单易用、主从普及、互联网案例多;复杂 SQL、CTE 历史较弱(8.0 已改善)
  • PG:标准 SQL、窗口函数、CTE、扩展生态;同样规格下复杂查询往往更稳
  • 选型:团队熟悉度 + 云厂商支持 + 是否需要 PG 特有扩展
原则:没有万能库。OLTP 与 OLAP 分离是常态;用同步链路连接,而不是用 MySQL 硬跑亿级 GROUP BY。

数据库面试回答模板

问题:MySQL 和 ClickHouse 怎么分工

MySQL 做业务 OLTP,保证事务和点查更新。ClickHouse 接 CDC 或日志流做 OLAP,承担大范围聚合和报表。原因是存储模型不同:行存适合短事务,列存适合扫描聚合。混用会拖垮在线库,也发挥不出 CK 的压缩和向量化优势。

问题:Hologres 和 ClickHouse 区别

两者都面向分析,但 Hologres 强调实时写入与 PG 协议兼容,和 Flink、MaxCompute 组成阿里云实时数仓,并支持主键高 QPS 点查,偏 HTAP Serving。ClickHouse 开源生态广,单表聚合和成本极致,但高频更新弱,更适合日志指标类场景。选型看是否已在阿里云实时链路、是否需要 PG 兼容接口。

问题:为什么报表不用 MySQL 直接查

报表多为大范围扫描和聚合,InnoDB 行存会读大量无用列,锁和 buffer pool 压力大,影响 OLTP。应通过汇总表、从库或同步到 ClickHouse/Hologres 等 OLAP 引擎,配合分区、预聚合和列存压缩,既快又降本。

常见追问

  • ClickHouse 为何快?列存、压缩、向量化执行、少随机 IO、MPP。
  • PG 的 MVCC?每行多版本,读不阻塞写,需 VACUUM 回收死元组。
  • HTAP 是什么?一套系统同时扛 OLTP+OLAP,如 TiDB、Hologres 部分场景;一般要接受架构或性能权衡。
  • CDC 是什么?捕获数据库变更日志同步到数仓,保证分析侧近实时。

HTTP 协议总览

HTTP(HyperText Transfer Protocol)是应用层协议,基于 TCP(HTTP/3 基于 QUIC/UDP),采用请求-响应模型,默认无状态(状态靠 Cookie/Session/Token 维护)。

报文结构

请求:
请求行   GET /api/user HTTP/1.1
首部行   Host: example.com
         Content-Type: application/json
空行
消息体   {"id":1}

响应:
状态行   HTTP/1.1 200 OK
首部行   Content-Type: application/json
         Content-Length: 15
空行
消息体   {"name":"Tom"}

一次完整交互(简化)

  1. DNS 解析域名 → IP
  2. TCP 三次握手建立连接(HTTPS 再 TLS 握手)
  3. 客户端发送 HTTP 请求
  4. 服务端处理并返回响应
  5. 连接关闭(HTTP/1.0)或复用(Keep-Alive / HTTP/2 多路复用)

核心特性

  • 无状态:服务器不保存客户端上下文,利于水平扩展
  • 灵活可扩展:首部可扩展;REST 用 URI + 方法表达资源
  • 明文/加密:HTTP 明文;HTTPS = HTTP + TLS

方法与状态码

常用方法(REST 语义)

方法语义幂等安全典型用途
GET获取资源查询,不应有副作用
POST提交/创建创建资源、表单提交
PUT全量更新/替换整体替换资源
PATCH部分更新否*局部字段更新
DELETE删除删除资源
HEAD同 GET 无 body检查资源是否存在、元数据
OPTIONS查询支持的方法CORS 预检

* PATCH 幂等性视实现而定,面试可说「通常不保证」。

常见状态码

类别含义
1xx100Continue,继续发送 body
2xx 成功200OK
201Created,POST 创建成功
204No Content,成功无 body(DELETE 常见)
3xx 重定向301永久重定向,SEO 权重转移
302/307临时重定向(307 保持方法)
304Not Modified,协商缓存命中
4xx 客户端400Bad Request,参数错误
401Unauthorized,未认证
403Forbidden,无权限
404Not Found
409Conflict,资源冲突
429Too Many Requests,限流
5xx 服务端500Internal Server Error
502Bad Gateway,网关上游异常
503Service Unavailable,过载/维护
504Gateway Timeout,上游超时
401 vs 403:401「你是谁」→ 要登录;403「你是谁我知道,但不能访问」。

HTTP 版本演进

版本关键特性问题/改进
HTTP/1.0 每次请求新建 TCP 连接 连接开销大
HTTP/1.1 默认 Keep-Alive 持久连接;管道化(pipelining,少用了);分块传输 Chunked 队头阻塞(同一连接上请求串行应答)
HTTP/2 二进制分帧、多路复用、头部压缩 HPACK、服务端推送(Push,现较少用) TCP 层仍可能队头阻塞;单连接多 Stream
HTTP/3 基于 QUIC(UDP),独立 Stream,连接迁移 解决 TCP 队头阻塞,弱网更稳

队头阻塞(HOL Blocking)

  • HTTP/1.1:同一连接多个请求排队等待响应
  • HTTP/2:应用层多路复用,但 TCP 丢包会阻塞整个连接所有 Stream
  • HTTP/3:QUIC 在传输层按 Stream 独立重传,减轻 HOL

连接管理

  • Connection: keep-alive(1.1 默认持久)
  • 浏览器对同一域名并发连接数有限(HTTP/1.1 常 6 个),故雪碧图、域名分片;HTTP/2 单连接多路复用缓解

首部与缓存

常见请求首部

  • Host:虚拟主机必选(1.1)
  • User-AgentAcceptAccept-Encoding: gzip
  • Authorization: Bearer <token> / Basic 认证
  • Cookie:携带会话
  • Content-Type / Content-Length
  • If-None-MatchIf-Modified-Since:协商缓存验证

常见响应首部

  • Set-Cookie:HttpOnly、Secure、SameSite 防 XSS/CSRF
  • Cache-Control:缓存策略核心
  • ETag / Last-Modified:协商缓存标识
  • Location:重定向 URL

缓存机制

类型机制首部
强缓存 未过期直接用本地,不请求服务器 Cache-Control: max-age=3600;过期看 Expires(优先级低)
协商缓存 过期后问服务器是否变化,未变则 304 ETag + If-None-Match;或 Last-Modified + If-Modified-Since

优先级:Cache-Control > ExpiresETag 比时间戳更准。

Content-Type 常考

  • application/json:REST API
  • application/x-www-form-urlencoded:表单默认
  • multipart/form-data:文件上传
  • text/htmlapplication/octet-stream

CORS(跨域)

  • 浏览器同源策略:协议+域名+端口相同
  • 简单请求:直接发,响应带 Access-Control-Allow-Origin
  • 预检请求:OPTIONS 先发,带 Access-Control-Request-Method

HTTPS 与 TLS

HTTPS = HTTP + TLS(SSL 已淘汰),提供机密性、完整性、身份认证

TLS 握手(1.2 简化)

  1. Client Hello:支持的 TLS 版本、加密套件、随机数
  2. Server Hello:选定参数、证书(含公钥)、随机数
  3. 客户端验证证书(CA 链、域名、有效期)
  4. 双方用非对称加密协商出对称会话密钥(premaster secret)
  5. 后续 HTTP 数据用对称加密(AES 等)传输

TLS 1.3 改进

  • 握手往返更少(1-RTT,甚至 0-RTT 恢复会话)
  • 禁用弱加密套件,更安全更快

相关概念

  • 对称加密:加解密同一密钥,速度快,传数据用
  • 非对称加密:公钥加密、私钥解密,握手用
  • 证书:CA 对公钥+主体签名,防中间人
  • HSTS:强制浏览器 HTTPS 访问,防降级攻击

HTTP 面试回答模板

问题:HTTP 和 HTTPS 区别

HTTP 是明文应用层协议,默认 80 端口。HTTPS 在 HTTP 与 TCP 之间加入 TLS,加密传输并校验完整性,默认 443,通过证书验证服务器身份,防止窃听和中间人攻击。

问题:GET 和 POST 区别

语义上 GET 安全幂等,用于获取资源;POST 非幂等,用于提交数据。实现上 GET 参数多在 URL,POST 多在 body;GET 可被缓存、收藏;POST 无长度理论限制(看服务器配置)。不应再用「POST 比 GET 安全」概括,敏感数据都应 HTTPS。

问题:HTTP/1.1 和 HTTP/2 区别

1.1 文本协议,持久连接但仍有队头阻塞。2.0 二进制分帧、单连接多路复用多个 Stream 并行,头部 HPACK 压缩,减少延迟。2 仍跑在 TCP 上,丢包会阻塞整连接;3.0 用 QUIC 缓解。

问题:强缓存和协商缓存

强缓存未过期直接用本地,看 Cache-Control max-age。过期后走协商缓存,带 If-None-Match 或 If-Modified-Since 问服务器,未变返回 304 用本地副本,变了返回 200 新内容。ETag 比 Last-Modified 精确。

常见追问

  • TCP 三次握手?SYN → SYN+ACK → ACK,确认双方收发能力。
  • Cookie 和 Session?Cookie 存客户端;Session 存服务端,Cookie 带 SessionId。
  • JWT 和 Session?JWT 无状态、服务端不存;需注意过期与吊销。
  • 502 和 504?502 上游无效响应;504 上游超时。

请求全链路总览

用户在浏览器点击按钮或输入 URL,到后端返回 JSON/HTML,再渲染到屏幕——中间经过浏览器、DNS、TCP/TLS、CDN、Nginx/Gateway、应用服务器、框架、业务代码、数据库/缓存/MQ等多层。面试要求能按层讲清每一跳在做什么、耗时在哪、故障点在哪

分层模型(自顶向下)

层级组件核心职责
1. 用户交互浏览器 / App WebView事件、JS、发起请求、解析响应、渲染
2. 前端运行时JS 引擎、Fetch/XHR、Service Worker组装 HTTP、Cookie、CORS、缓存策略
3. 网络DNS、TCP、TLS、HTTP寻址、可靠传输、加密、应用层语义
4. 边缘CDN、WAF、DDoS 防护静态加速、缓存、安全过滤
5. 接入Nginx / SLB / API Gateway反向代理、负载均衡、SSL 终结、限流
6. 应用Tomcat / Netty / Spring Boot线程模型、Filter、Controller、业务逻辑
7. 数据MySQL、Redis、MQ、ES持久化、缓存、异步、搜索
8. 回程序列化 → HTTP 响应 → 浏览器渲染压缩、分块、DOM/CSS/JS 执行

一次 API 请求的时间线(典型 HTTPS)

0ms    用户点击 → JS 事件处理
1ms    浏览器查 DNS 缓存 / 发起 DNS 查询
20ms   TCP 三次握手(或复用 Keep-Alive 连接跳过)
40ms   TLS 1.3 握手(或 Session Ticket 复用跳过)
45ms   浏览器发送 HTTP 请求(Header + Body)
50ms   CDN 未命中 → 回源 Nginx
55ms   Nginx 转发 → Gateway → 业务 Pod
60ms   Spring Filter 链 → Controller → Service
65ms   Redis 命中 / MySQL 查询 5ms
70ms   组装 JSON 响应
100ms  响应到达浏览器 → JSON.parse → 更新 DOM
120ms  用户看到结果
首包慢往往是 DNS + TCP + TLS;接口慢往往是 DB、下游 RPC 或锁竞争。要能区分「网络」与「服务端处理」耗时。

浏览器侧生命周期

1. 用户触发

  • 地址栏输入 URL:Navigation 开始,可能触发 HSTS 强制 HTTPS
  • 点击链接触发:默认 GET;<form submit> 可能 POST
  • JS 主动请求:fetch() / axios / XMLHttpRequest

2. 同源策略与 CORS

  • 前端页面 https://a.com 请求 https://b.com/api跨域
  • 简单请求:直接发,响应需带 Access-Control-Allow-Origin
  • 非简单请求(自定义 Header、PUT/DELETE、JSON Content-Type 等):浏览器先发 OPTIONS 预检
  • 预检通过后才会发真正的业务请求 → 实际多一次 RTT

3. Cookie 与凭证

  • 同域请求默认带 Cookie;跨域需 fetch(..., { credentials: 'include' }) 且服务端 CORS 允许
  • HttpOnly Cookie 防 XSS 窃取;Secure 仅 HTTPS;SameSite 防 CSRF
  • JWT 常放 Authorization: Bearer Header,由 JS 或 BFF 写入

4. 浏览器缓存(发请求前)

类型行为关键 Header
强缓存未过期直接用本地,不发网络请求Cache-Control: max-age
协商缓存过期后带 ETag/Last-Modified 问服务器,304 用本地If-None-Match / If-Modified-Since
Service Worker可拦截 fetch,走自定义缓存策略

5. 组装 HTTP 请求

POST /api/order HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbG...
Cookie: sessionId=abc; token=xyz
Accept: application/json
Content-Length: 42

{"skuId":1001,"count":1}
  • 浏览器/network 栈负责:URL 解析、Header 拼接、Body 编码、Cookie 自动附加
  • HTTP/2:多路复用,同一 TCP 连接上并行多个 Stream
  • HTTP/3:基于 QUIC(UDP),减少握手与队头阻塞

网络传输层

1. DNS 解析

  1. 浏览器 DNS 缓存 → 操作系统缓存 → /etc/hosts
  2. 本地 DNS 递归查询 → 根 → 顶级域 → 权威 DNS
  3. 得到 IP(A 记录)或 CNAME 再递归

优化:DNS 预解析 <link rel="dns-prefetch">、合理 TTL、HTTPDNS(App 场景)。

2. TCP 三次握手

Client ──SYN(seq=x)──────────────► Server
Client ◄──SYN+ACK(seq=y,ack=x+1)── Server
Client ──ACK(ack=y+1)───────────► Server
→ 连接建立,可传 HTTP
  • Keep-Alive:同一连接复用,避免每次请求都三次握手
  • 队头阻塞:HTTP/1.1 同一连接上请求需排队等响应(HTTP/2 多路复用缓解应用层,TCP 层仍可能阻塞)

3. TLS 握手(HTTPS)

TLS 1.3 典型 1-RTT 握手(首次 2-RTT),之后 Session Ticket / Session ID 可0-RTT 复用(有重放风险需评估)。

  • 协商 cipher suite、交换密钥、验证服务器证书链(CA → 中间 CA → 站点证书)
  • 证书校验:域名匹配、有效期、吊销列表(OCSP)
  • 之后 HTTP 报文在 TLS 记录层加密传输

4. HTTP 请求到达服务器

操作系统内核 TCP 缓冲区收包 → 监听端口进程(Nginx :443 / Tomcat :8080)read → 解析 HTTP 报文。

5. 连接关闭

  • HTTP/1.1 默认 Keep-Alive,空闲超时后 TCP 四次挥手
  • HTTP/2 长连接 + 多 Stream;GOAWAY 帧优雅关闭

CDN 与接入层(Nginx)

CDN 路径

  1. DNS 解析到 CDN 边缘节点 IP(CNAME 到 CDN 域名)
  2. 边缘节点查缓存:静态资源(JS/CSS/图片)命中则直接 200 返回
  3. 未命中:回源到源站 Nginx/OSS
  4. 动态 API 通常不缓存或极短 TTL,CDN 只做传输加速

Nginx 反向代理(请求进入后端前)

server {
  listen 443 ssl;
  server_name api.example.com;

  ssl_certificate     /path/fullchain.pem;
  ssl_certificate_key /path/privkey.pem;

  location /api/ {
    limit_req zone=api burst=20 nodelay;   # 限流
    proxy_pass http://gateway_upstream;   # 转发上游
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Nginx 处理步骤

  1. SSL 终结:对外 HTTPS,对内 often HTTP(内网可信)或继续 HTTPS
  2. 解析请求行与 Header
  3. location 匹配:静态文件直接 root/alias;API 走 proxy_pass
  4. 负载均衡:轮询、加权、ip_hash、least_conn 选 upstream 一台
  5. 附加 Header 转发给后端,便于日志与风控
  6. 等待上游响应,可 gzip 压缩后返回客户端

WAF / 安全(可选)

  • SQL 注入、XSS 特征拦截;CC 攻击频率限制
  • 在 Nginx 前或云 WAF 层完成

API 网关与负载均衡

SLB / L4 负载均衡

  • 工作在传输层,按 IP+端口转发到后端 Nginx 或 Pod
  • 健康检查剔除故障节点

Spring Cloud Gateway(L7)

  1. 请求到达 Gateway(WebFlux 事件循环)
  2. Global Filter 链:日志 TraceId、JWT 鉴权、限流、黑名单
  3. Route 匹配:Path=/api/order/**lb://order-service
  4. Gateway Filter:StripPrefix、AddRequestHeader、RequestRateLimiter
  5. LoadBalancer:从 Nacos/Eureka 取实例列表,选一台发 HTTP
  6. 收下游响应,经 Filter 后返回 Nginx → 客户端
Client → Nginx → Gateway → [Auth Filter] → [Route Match]
  → LoadBalancer 选实例 10.0.1.5:8080
  → HTTP 转发到 order-service Pod
  ← 响应沿原路返回

网关常见职责

职责说明
统一入口对外一个域名,内部多微服务
鉴权校验 JWT,下游信任或二次校验
限流熔断Sentinel / RequestRateLimiter
灰度路由Header 标签 → 金丝雀实例
协议转换HTTP ↔ gRPC(部分网关支持)

Spring Boot 后端处理

以 Tomcat + Spring MVC 为例(Servlet 栈);WebFlux 为事件驱动模型,链路概念类似但无线程一请求一绑定。

Tomcat 接收请求

  1. Acceptor 线程 accept 新连接 → Poller 监听可读
  2. Worker 线程从 socket read HTTP 报文
  3. 封装为 HttpServletRequest / HttpServletResponse
  4. 交给 Servlet 容器处理链

Filter 链(Servlet Filter)

Request → CharacterEncodingFilter
       → CorsFilter
       → SpringSecurityFilterChain(认证/授权)
       → TraceFilter(链路追踪)
       → DispatcherServlet
  • Filter 可短路:未登录返回 401,不再进 Controller
  • 顺序由 @Order 或 RegistrationBean 决定

DispatcherServlet 核心流程

  1. HandlerMapping:根据 URL + Method 找 Handler(Controller 方法),如 @GetMapping("/api/user/{id}")
  2. HandlerAdapter:调用 Controller,解析 @PathVariable@RequestBody@RequestParam
  3. Controller:参数校验(@Valid)→ 调 Service
  4. Service:业务逻辑、事务(@Transactional)、调 Mapper/Feign
  5. 返回:对象 → HttpMessageConverter(Jackson)→ JSON 写回 Response

AOP 与事务

  • @Transactional 通过 AOP 代理:方法前开事务,异常回滚,提交后释放连接
  • 切面顺序:Security → Transaction → 业务,面试常问「事务失效」场景(自调用、非 public、异常被吞)

异常处理

  • @ControllerAdvice + @ExceptionHandler 统一返回 {code, message}
  • 未捕获异常 → 500;参数错误 → 400

数据层与中间件

连接池(HikariCP)

  1. Service 需要 DB 连接 → 从池 borrow
  2. 执行 SQL → ResultSet 映射为 Java 对象(MyBatis / JPA)
  3. 归还连接到池(勿在 finally 泄漏)

池耗尽 → 请求阻塞等待 → RT 飙升 → 线程池打满 → 雪崩。

MySQL 一次查询

  1. 解析 SQL → 优化器选索引 → 存储引擎 InnoDB
  2. Buffer Pool 命中页则内存读,否则磁盘 IO
  3. 加行锁(UPDATE)/ 间隙锁(范围)→ 返回结果
  4. binlog 记录变更(主从复制、CDC)

Redis 一次访问

  • Lettuce/Jedis 发命令 → Redis 单线程执行 → 返回
  • 缓存旁路:先 Redis miss → 查 DB → 写 Redis
  • RT 通常 1ms 级,远快于 DB

Feign 调用下游微服务

  1. Feign 接口 → LoadBalancer 选实例 → HTTP 发下游
  2. 超时、重试(幂等才重试)、熔断(Sentinel/Resilience4j)
  3. 全链路 TraceId 通过 Header 传递(Sleuth/Micrometer)

异步 MQ(若涉及)

同步链路只负责「发消息成功」;Consumer 另线程消费,与 HTTP 请求生命周期分离,但面试常作为「下单后异步通知」的延伸。

响应回程与浏览器渲染

服务端组装响应

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Encoding: gzip
Set-Cookie: sessionId=...; HttpOnly; Secure
X-Trace-Id: abc123

{"code":0,"data":{"orderId":10001}}
  1. Controller 返回值 → Jackson 序列化
  2. Filter 可改 Header(CORS、Security)
  3. Nginx gzip 压缩 body
  4. TCP 分包发送 → 客户端 ACK

浏览器处理响应

  1. 解析状态码:2xx 成功;401 跳登录;5xx 提示错误
  2. 读 Header:Set-Cookie 更新 Cookie;Content-Type 决定解析方式
  3. gzip/br 解压 body
  4. JSON API:response.json() → JS 更新 state → 虚拟 DOM diff → 重绘
  5. HTML 导航:解析 HTML → 构建 DOM → 加载 CSS/JS → 执行脚本 → 渲染树 → 绘制

Performance API 可观测

  • performance.getEntriesByType('navigation'):DNS、TCP、TTFB、DOMContentLoaded
  • TTFB(首字节时间):网络 + 服务端处理
  • 服务端日志:Gateway access log + 应用 Trace span 分解耗时

微服务场景扩展链路

单体一次请求只进一个 JVM;微服务可能串行/并行多次 RPC

Browser
  → Nginx → Gateway
      → order-service(写订单)
          → Feign → product-service(查库存)
          → Feign → user-service(查用户)
          → Redis(扣库存 Lua)
          → RocketMQ(发消息)
      ← 聚合结果 JSON
  ← 返回浏览器

MQ Consumer(异步,另一生命周期)
  → inventory-service 确认扣减
  → payment-service 发起支付

分布式追踪

  • Gateway 生成 TraceId → 各服务 Log/Span 关联
  • Jaeger / Zipkin / SkyWalking 展示调用瀑布图
  • 定位慢在哪个 Feign、哪条 SQL

与单体对比

维度单体微服务
网络 hop多,RT 叠加
故障点进程内任一下游超时/熔断
事务本地 @Transactional分布式事务 / 最终一致
部署一个包多 Pod + 注册中心

全链路面试回答模板

问题:描述一次 HTTP 请求从浏览器到后端的完整过程

用户在浏览器触发请求后,先走 DNS 解析得到 IP,TCP 三次握手,HTTPS 还要 TLS 握手。浏览器组装 HTTP 报文,带上 Cookie 或 Authorization。请求可能经 CDN 缓存静态资源;动态 API 到 Nginx 做 SSL 终结、限流和反向代理,再转发到 API Gateway。Gateway 做鉴权、路由和负载均衡,选中一个 Spring Boot 实例。Tomcat Worker 线程接收请求,经过 Filter 链和 DispatcherServlet,匹配到 Controller,进 Service 和 Mapper,可能访问 Redis 和 MySQL,再序列化成 JSON 沿原路返回。浏览器解析 JSON 更新页面。我会补充 Keep-Alive 复用连接、CORS 预检、以及慢请求用 TTFB 和链路追踪定位瓶颈。

问题:HTTPS 比 HTTP 多做了什么

在 TCP 之上增加 TLS 层:证书校验服务器身份、协商对称密钥、之后 HTTP 内容加密传输,防窃听和篡改。首次有额外握手 RTT,TLS 1.3 和 Session 复用可优化。Nginx 可在边缘做 SSL 终结,内网 HTTP 减轻后端加解密开销。

问题:请求慢怎么排查

先分客户端还是服务端:看浏览器 Network 的 Waiting/TTFB。TTFB 高则查 Nginx/Gateway/应用日志和 Trace。服务端用 APM 看 Controller、SQL、Redis、Feign 各段耗时。常见瓶颈:慢 SQL、连接池满、下游 RPC 超时、GC STW、锁竞争。客户端侧查 DNS、跨域预检、包体过大、未开 gzip。

问题:Nginx 和 Gateway 分别做什么

Nginx 常作最外层反向代理和 SSL 终结、静态资源、四层/七层负载、限流。Gateway 是应用层 API 网关,面向微服务做动态路由、JWT 鉴权、与注册中心集成、灰度和 Sentinel 限流。通常流量是 Client → Nginx → Gateway → 微服务,职责分层而非重复。

常见追问

  • OPTIONS 预检什么时候发?跨域且非简单请求时,先于 POST/PUT 等发 OPTIONS。
  • 502 和 504?502 上游返回非法响应或连接被拒;504 网关等上游超时。
  • Tomcat 线程模型?Acceptor + Poller + Worker;Worker 阻塞 IO 时线程占满则拒绝新请求。
  • 为什么 Gateway 用 WebFlux?非阻塞 IO,少量线程扛高并发转发;阻塞调用需隔离线程池。
  • Cookie 什么时候带上?同域自动带;跨域需 credentials + CORS 允许;Domain/Path 决定范围。

SSO 总览

单点登录(Single Sign-On,SSO):用户在一个统一认证中心(IdP / Auth Server)完成一次登录后,访问同一信任域内的多个应用(SP / 业务系统)时无需重复输入账号密码

核心目标不是「少输一次密码」,而是认证逻辑集中化:账号体系、密码策略、MFA、审计、会话吊销、权限模型在一处治理,业务系统只关心「这个请求代表谁、有什么权限」。

SSO 解决什么问题

  • 用户体验:多系统一次登录,减少重复认证
  • 安全治理:统一 MFA、密码策略、登录风控、会话超时
  • 运维成本:账号开通/离职回收一处处理,避免各系统各自维护用户表
  • 架构解耦:业务系统不存密码,只验证 Token / Ticket

与相关概念的区别

概念关注点典型场景
SSO 一次登录,多系统通行 企业 OA + CRM + 内部后台
OAuth 2.0 授权第三方代用户访问资源(delegation) 「用 GitHub 登录」、开放平台 API
OpenID Connect 在 OAuth2 之上补身份认证(ID Token) 现代 Web / 移动端统一登录
SAML 2.0 基于 XML 断言的企业联邦身份 企业 IdP 对接 SaaS(如 Okta → 各应用)
CAS Ticket 票据 + 重定向,协议简单 高校、传统 Java 门户
面试常问:OAuth2 是授权框架,不等于 SSO;但 OIDC 授权码模式 是当前实现 SSO 的主流方案。

核心概念

角色模型

角色英文职责
用户Resource Owner最终使用者
认证中心IdP / Authorization Server校验身份、签发 Token/Ticket、管理会话
业务应用SP / Relying Party / Client消费认证结果,建立本地登录态或信任 Token
API 资源Resource Server校验 Access Token,提供受保护 API
浏览器 / AppUser Agent承载重定向、Cookie、PKCE 等交互

凭证形态对比

方式存储位置优点缺点
Session + Cookie Session 在服务端,SessionId 在 Cookie 易吊销、服务端可控 跨域、跨子域、分布式 Session 需 Redis 等
JWT Token 自包含,可放 Header/Cookie 无状态、易水平扩展 主动吊销难,需黑名单/短过期 + Refresh
Opaque Token 随机串,服务端查 Redis/DB 易吊销、可审计 每次校验需查存储或 introspection
Ticket(CAS) TGT 在 SSO 域 Cookie,ST 一次性 协议成熟、SP 改造小 重定向链长,纯 API 场景不友好

信任域(Trust Domain)

SSO 生效前提是 SP 信任 IdP 签发的凭证。信任建立方式:

  • 共享密钥 / 公钥(JWT 验签、SAML 证书)
  • 预注册的 ClientId + Redirect URI 白名单
  • 网关统一验 Token,内网服务信任网关传递的用户上下文

协议选型

选型建议(2020s 常见实践)

场景推荐说明
新建 Web / 移动端 SSOOIDC 授权码 + PKCEKeycloak、Authing、Auth0、自建 Spring Authorization Server
企业对接 SaaS / 多云SAML 2.0 或 OIDC看 SaaS 支持;传统企业 IdP 多 SAML
纯后端 API / 微服务JWT + Gateway 统一鉴权Token 由 IdP 签发,Resource Server 验签或 introspection
遗留门户、高校系统CAS改造成本低,与 OIDC 可并存
第三方「社交登录」OAuth2(非完整 SSO)仅作登录入口,仍需绑定本地账号体系

OIDC vs SAML(面试高频)

OIDCSAML 2.0
数据格式JSON / JWTXML 断言
传输HTTP REST、重定向HTTP POST 绑定、Redirect
移动端友好(PKCE)较弱
企业 IdP增长快存量多

典型流程

1. OIDC 授权码模式(Web SSO 主流)

  1. 用户访问 SP(业务系统),未登录 → 302 到 IdP /authorize
  2. 用户在 IdP 登录(或已有 SSO Cookie,直接跳过)
  3. IdP 302 回 SP 的 redirect_uri,携带一次性 code
  4. SP 后端用 code + client_secret(或 PKCE 的 verifier)向 IdP /token 换 Token
  5. SP 得到 id_token(身份)+ access_token(访问 API)+ 可选 refresh_token
  6. SP 建立本地 Session 或把 Token 写入 HttpOnly Cookie,后续请求带 Session / Bearer Token
用户 ──► SP ──302──► IdP /authorize?client_id=&redirect_uri=&scope=openid
用户 ◄── IdP 登录页(或 SSO Cookie 免登)
用户 ◄── SP /callback?code=xxx
SP  ──POST──► IdP /token  (后端,不经过浏览器传 secret)
SP  ◄── id_token + access_token
SP  建立会话,访问受保护资源
Public Client(SPA / 移动端)必须用 PKCE,不能把 client_secret 放前端。

2. CAS 单点登录(简化)

  1. 用户访问 SP-A,无本地 Session → 重定向到 CAS /login?service=SP-A
  2. CAS 校验凭证,写入 TGT(存在 CAS 域 Cookie)
  3. CAS 签发一次性 ST(Service Ticket),重定向回 SP-A
  4. SP-A 后端拿 ST 调 CAS /serviceValidate 校验,得到用户名
  5. 用户再访问 SP-B,同样重定向 CAS;因 TGT 仍在,免输密码,直接发 ST 给 SP-B

3. 网关统一鉴权(微服务常见)

  • 浏览器只与 API GatewayIdP 交互
  • Gateway Global Filter 校验 JWT(签名、过期、audience、scope)
  • 校验通过后向下游注入 Header(如 X-User-IdX-Roles)或透传原始 Token
  • 内网服务策略:全信任网关 / 二次验签 / 仅内网 mTLS

4. 单点登出(SLO)

SSO 登录容易,登出更难:IdP 会话、各 SP 本地 Session、Refresh Token 可能并存。

  • IdP 发起:IdP 登出后通知各 SP 的 logout URL(Front-Channel iframe / Back-Channel POST)
  • SP 发起:SP 清本地 Session 后重定向 IdP /logout
  • Token 方案:短 Access Token + Refresh 轮换;登出时吊销 Refresh / 加入黑名单

架构设计

推荐分层

身份层(IdP)

用户目录(LDAP/AD/自建)、认证、MFA、Token 签发、会话、审计。例:Keycloak、Authing、Okta、Spring Authorization Server。

接入层(BFF / Gateway)

OAuth 回调、Token 交换、Cookie 下发、CORS、限流。避免 SPA 直接持有 long-lived Token。

业务层(SP)

只消费已认证身份,维护业务权限(RBAC/ABAC),不存密码。

资源层(API)

验 Access Token 或信任网关 Header;细粒度 scope / 权限校验。

多系统账号映射

  • 统一账号:全系统同一 userId(最理想)
  • 联邦映射:IdP 的 sub 与各系统 local_user 表关联
  • JIT Provisioning:首次 SSO 登录自动在 SP 建用户
  • SCIM:IdP 向各 SP 同步用户生命周期(入职/离职)

跨域与子域 Cookie

  • 同主域:.example.com 共享 Cookie 可实现「伪 SSO」
  • 跨主域:必须走标准协议(OIDC/SAML/CAS),不能单靠 Cookie
  • 生产环境:SecureHttpOnlySameSite=Lax/Strict,防 CSRF 与 Token 泄露

与 Spring 生态集成(思路)

# 业务服务作为 Resource Server
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://idp.example.com/realms/corp

# Gateway 统一鉴权 + 路由
spring.cloud.gateway.routes[0].filters=TokenRelay, StripPrefix=1

# BFF 用 OAuth2 Login 接 OIDC,浏览器只拿 Session Cookie

安全与治理

必做项

  • HTTPS 全链路,Redirect URI 严格白名单
  • 授权码模式,Public Client 配 PKCE;禁止 Implicit 模式存 Token 到 URL
  • state 防 CSRF;nonce 防 ID Token 重放
  • Access Token 短过期(5–15 min),Refresh Token 轮换 + 吊销表
  • 敏感操作二次验证(MFA、step-up auth)
  • 登录审计:IP、设备、失败次数、异地风控

JWT 在 SSO 中的注意点

  • JWT 默认无法服务端主动失效,登出/改密需配合短 TTL、黑名单、版本号(token version)
  • 不要把敏感信息放 JWT payload(可解码)
  • 验签:RS256(IdP 私钥签,SP 公钥验)优于 HS256 共享密钥
  • 校验 issaudexpnbf,防跨租户 Token 误用

常见坑

问题原因对策
登出后仍能访问SP 本地 Session 未清 / JWT 仍有效SLO + 短 Token + 黑名单
开放重定向redirect_uri 校验不严精确匹配 + 禁止通配
Token 放 localStorageXSS 可窃取HttpOnly Cookie 或 BFF 模式
各系统权限不一致只有认证没有统一授权模型IdP 管身份,SP 或权限中心管 RBAC
时钟偏移JWT exp 校验失败NTP 同步,允许小 skew

SSO 面试回答模板

问题:什么是单点登录?怎么设计?

单点登录是用户在统一认证中心登录一次,即可访问多个互信业务系统而无需重复认证。设计上我会拆成 IdP 负责身份认证与 Token 签发,各 SP 只验证凭证并建立本地会话或信任 JWT。现代 Web 优先 OIDC 授权码 + PKCE:浏览器拿 code,SP 后端换 Token,避免 secret 和 Token 暴露在前端。微服务场景还会在 Gateway 做统一 JWT 校验,下游服务只处理业务权限。

问题:OAuth2、OIDC、SSO 什么关系?

OAuth2 解决的是授权——让第三方应用在用户同意下访问资源,本身不定义身份认证。OIDC 在 OAuth2 上增加 ID Token 和标准 UserInfo,专门表达「用户是谁」。SSO 是业务目标;实现上常用 OIDC 授权码流程,由 IdP 维护 SSO Session,多个 Client 依次完成 code 交换即可免登。

问题:JWT 和 Session 做 SSO 怎么选?

Session 适合服务端可控、强调吊销和审计的场景,但分布式要 Redis 共享 Session。JWT 适合无状态微服务和跨域 API,但要接受主动吊销难,需短过期 + Refresh 或 Token 黑名单。很多架构是混合:浏览器对 BFF 用 HttpOnly Session Cookie,BFF 对后端用 JWT 或内部 mTLS。

问题:如何实现单点登出?

单点登出要同时销毁 IdP 全局会话和各 SP 本地会话。SAML/OIDC 有 Front-Channel 和 Back-Channel Logout 规范;CAS 有 SLO 通知。JWT 方案则依赖短 Access Token、Refresh Token 吊销和网关黑名单。实际项目常做「IdP 登出 + 关键 SP 回调」,并接受完全即时全网失效有延迟。

常见追问

  • PKCE 是什么?Proof Key for Code Exchange,Public Client 用 code_verifier 换 code,防授权码被截获后滥用。
  • ID Token 和 Access Token?ID Token 表身份(给 Client);Access Token 访问 Resource Server API。
  • 同域 Cookie 算 SSO 吗?同主域共享 Session Cookie 可算轻量 SSO,但不跨域、难对接第三方,生产仍推荐 IdP 协议。
  • Gateway 鉴权后下游还要验吗?看信任边界:零信任下 Resource Server 仍验 JWT;纯内网可信任网关 Header 但要防绕过。