Java 高频面试题 100 道详解-大厂真题汇总

Cosolar 6 阅读 后端

覆盖 Java 基础、并发编程、JVM、MySQL、Redis、消息队列、分布式系统、Spring 生态、微服务架构、场景设计等 10 大方向。每题包含核心原理、详细解析、生产场景与解决方法

整理日期:2026-07-04 | 来源:CSDN、掘金、阿里云开发者社区、腾讯云开发者社区、51CTO、JavaGuide 等技术平台真实面试题整理

一、Java 基础(Q1-Q10)

Q1:==equals() 的区别是什么?

核心原理: == 比较的是内存地址(引用是否指向同一对象),equals() 比较的是值内容(取决于类是否重写了 equals 方法)。

详细解析:

  • 对于基本数据类型(int、char 等),== 比较的是值本身。
  • 对于引用数据类型(String、Object 等),== 比较的是对象的内存地址。
  • Object 类的默认 equals() 实现等同于 ==,即比较地址。StringInteger 等类重写了 equals() 方法,改为比较值内容。
  • Integer-128~127 范围内有缓存(Integer Cache),此范围内 == 返回 true,超出范围则返回 false。

生产场景: 在支付系统中比较两个订单号是否相同,必须用 equals() 而非 ==。曾经有线上 Bug:两个超出 Integer 缓存范围的订单 ID 用 == 比较,导致本应匹配的订单未被匹配,用户重复支付。解决方法: 所有对象值比较统一使用 equals(),并在 Code Review 中将 == 对象比较列为检查项。

Q2:String、StringBuilder、StringBuffer 的区别?

核心原理: String 不可变(final 修饰),StringBuilder 可变且非线程安全,StringBuffer 可变且线程安全(synchronized 修饰)。

详细解析:

  • String:底层 final char[](JDK9 后改为 byte[]),每次拼接都会创建新对象,产生大量临时对象。
  • StringBuilder:继承 AbstractStringBuilder,无同步锁,单线程下字符串拼接性能最优。
  • StringBuffer:每个方法都加了 synchronized,线程安全但性能比 StringBuilder 差约 10%-20%。
  • 编译器对 + 拼接做了优化:少量拼接会编译为 StringBuilder.append(),但循环内拼接每次循环都会 new StringBuilder,需手动用 StringBuilder。

生产场景: 日志系统中拼接大量日志字段,如果用 String + 拼接,在循环中每轮创建一个 StringBuilder 对象,GC 压力极大。解决方法: 循环内拼接用 StringBuilder,预分配容量 new StringBuilder(128) 避免扩容。JSON 序列化场景优先用 StringBuilder。

Q3:HashMap 底层实现原理(JDK 1.8)?

核心原理: JDK 1.8 的 HashMap 底层是数组 + 链表 + 红黑树,通过哈希函数定位数组下标,哈希冲突时用链地址法(拉链法)解决。

详细解析:

  • 初始化:默认数组长度 16,负载因子 0.75,阈值 = 数组长度 × 负载因子 = 12。
  • put 流程:计算 key 的 hash 值((h = key.hashCode()) ^ (h >>> 16) 扰动处理)→ (n-1) & hash 定位下标 → 桶为空直接放入 → 桶不为空则遍历链表/红黑树,key 相同则覆盖,不同则尾插。
  • 链表转红黑树:链表长度 > 8 且数组长度 ≥ 64 时,链表转红黑树(时间复杂度从 O(n) 降为 O(log n));红黑树节点 ≤ 6 时退化为链表。
  • 扩容机制:元素数量超过阈值时扩容为 2 倍,重新计算每个元素的位置(原位置原位置 + 旧容量)。
  • JDK 1.7 采用头插法,多线程扩容可能形成环形链表导致死循环;JDK 1.8 改为尾插法解决了此问题,但 HashMap 仍非线程安全。

生产场景: 高并发场景下使用 HashMap 导致数据丢失或 CPU 100%(JDK 1.7 死循环)。解决方法: 并发场景使用 ConcurrentHashMap,不要使用 Collections.synchronizedMap(性能差)。

Q4:ConcurrentHashMap 的线程安全原理(JDK 1.7 vs 1.8)?

核心原理: JDK 1.7 用分段锁(Segment + ReentrantLock),JDK 1.8 改为 CAS + synchronized 锁单个桶节点。

详细解析:

  • JDK 1.7:Segment 数组(默认 16 个段),每个 Segment 继承 ReentrantLock,锁粒度为段。并发度 = Segment 数量,最大 16 线程并发写。
  • JDK 1.8:去掉 Segment,直接用 Node 数组。初始化数组、插入空桶用 CAS(无锁乐观操作);发生哈希冲突时用 synchronized 锁住链表头节点/红黑树根节点。锁粒度从段降到单个桶,并发度大幅提升。
  • 读操作完全无锁:Node 的 val 和 next 用 volatile 修饰,保证可见性。
  • size() 方法用 LongAdder 思想:baseCount + CounterCell 数组分散计数,减少 CAS 竞争。

生产场景: 秒杀系统中缓存商品库存到本地 Map,多线程同时读写。解决方法: 使用 ConcurrentHashMap,配合 CAS 原子操作保证库存扣减的正确性。注意 size() 是近似值,不保证精确。

Q5:ArrayList 和 LinkedList 的区别?

核心原理: ArrayList 底层是动态数组,LinkedList 底层是双向链表。

详细解析:

  • ArrayList:随机访问 O(1),尾部插入均摊 O(1),中间插入/删除 O(n)(需移动元素)。扩容为 1.5 倍(oldCapacity + (oldCapacity >> 1)),扩容时用 Arrays.copyOf 复制数组。
  • LinkedList:随机访问 O(n)(需从头遍历),头尾插入/删除 O(1)。实现了 Deque 接口,可作为队列/双端队列使用。
  • 内存占用:ArrayList 连续内存,空间利用率高;LinkedList 每个节点额外存储前驱和后继指针,内存开销大。
  • ArrayList 实现了 RandomAccess 接口(标记接口),推荐用 for 循环遍历;LinkedList 未实现,推荐用迭代器遍历。

生产场景: 批量数据导入场景,如果预先知道数据量,new ArrayList<>(100000) 预分配容量避免多次扩容。解决方法: 频繁头插用 LinkedList 或 ArrayDeque;需要线程安全用 CopyOnWriteArrayList(读多写少)或 Collections.synchronizedList

Q6:Java 异常体系结构?

核心原理: Throwable 是所有异常的根类,分为 Error 和 Exception 两个分支。Exception 分为受检异常(Checked Exception)和非受检异常(RuntimeException)。

详细解析:

  • Error:程序无法处理的严重错误(OOM、StackOverflowError),JVM 会终止程序。
  • 受检异常:编译器强制要求处理(IOException、SQLException),除 RuntimeException 以外的 Exception 子类。
  • 非受检异常:编译器不强制处理(NullPointerException、ArrayIndexOutOfBoundsException),通常是编程错误。
  • try-catch-finally:finally 块无论是否异常都会执行(除 System.exit())。JDK 7 引入 try-with-resources 自动关闭实现 AutoCloseable 的资源。
  • 异常链:throw new RuntimeException("包装异常", e) 保留原始异常堆栈。

生产场景: 线上系统大量 catch(Exception e) 吞掉异常,导致问题难以排查。解决方法: 不要捕获大范围异常后忽略,至少记录日志;自定义业务异常体系(BaseException → BusinessException、SystemException),统一异常处理器(@ControllerAdvice + @ExceptionHandler)返回标准错误码。

Q7:Java 反射的原理与应用?

核心原理: 反射是在运行时动态获取类信息、创建对象、调用方法、访问字段的机制。JVM 加载类后会在方法区生成 Class 对象,反射通过 Class 对象操作类的元数据。

详细解析:

  • 获取 Class 对象三种方式:Class.forName("全限定名")对象.getClass()类名.class
  • 核心 API:getDeclaredFields() 获取所有字段(含 private)、getDeclaredMethods() 获取所有方法、getConstructor() 获取构造器、setAccessible(true) 突破访问控制。
  • 反射性能问题:反射调用比直接调用慢约 10-50 倍(需查找方法、安全检查、参数装箱)。可通过 Method.setAccessible(true) 关闭安全检查提速、或用 MethodHandle / ASM 字节码增强。
  • 应用场景:Spring IOC(反射创建 Bean)、MyBatis(反射映射结果集)、JSON 序列化(反射读取字段值)、动态代理。

生产场景: RPC 框架中需要根据接口名动态调用远程方法。解决方法: 缓存 Method 对象避免重复查找;高频调用场景用 ASM/CGLIB 生成字节码代理代替反射,性能接近直接调用。

Q8:JDK 动态代理 vs CGLIB 动态代理?

核心原理: JDK 动态代理基于接口实现(Proxy + InvocationHandler),CGLIB 基于继承实现(生成目标类的子类 + MethodInterceptor)。

详细解析:

  • JDK 动态代理:目标类必须实现接口,Proxy.newProxyInstance() 在运行时生成实现相同接口的代理类。通过 InvocationHandler.invoke() 拦截方法调用。
  • CGLIB 动态代理:不要求接口,通过 ASM 生成目标类的子类,重写非 final 方法。通过 MethodInterceptor.intercept() 拦截。不能代理 final 类和 final 方法。
  • 性能对比:CGLIB 创建代理慢(需生成字节码),但方法调用比 JDK 快(FastClass 机制避免反射)。JDK 代理创建快,但调用使用反射较慢。
  • Spring 默认策略:有接口用 JDK 代理,无接口用 CGLIB;spring.aop.proxy-target-class=true 强制用 CGLIB。

生产场景: Spring AOP 中 @Transactional 注解失效的经典问题:同类内部方法调用不经过代理对象,导致事务不生效。解决方法: 通过 AopContext.currentProxy() 获取当前代理对象调用,或将方法拆到不同类中。

Q9:Java 泛型擦除机制及其影响?

核心原理: Java 泛型在编译期做类型检查,编译后擦除泛型信息(替换为上界或 Object),运行时无法获取泛型类型。

详细解析:

  • 擦除规则:List<String>List<Integer> 运行时都是 List,泛型类型参数被擦除为 Object(无上界)或上界类型(有上界 <T extends Number> 擦除为 Number)。
  • 桥接方法:子类泛型方法重写父类泛型方法时,编译器生成桥接方法保证多态正确性。
  • 无法做的事:new T()(类型未知)、T.class(擦除后无 Class 对象)、instanceof List<String>(擦除后无法区分)、基本类型泛型(只能用包装类)。
  • 获取泛型类型的方法:通过子类继承的泛型参数(Class.getGenericSuperclass())、通过方法返回值类型(Method.getGenericReturnType())。

生产场景: JSON 反序列化时 List<User> 无法直接传入泛型类型,导致返回 List<LinkedHashMap> 而非 List<User>解决方法: 使用 TypeReference(Jackson)或 TypeReference<T>(Fastjson)保留泛型类型信息,或传入 Class<User[]> 用数组绕过擦除。

Q10:Java 中的 SPI 机制是什么?

核心原理: SPI(Service Provider Interface)是一种服务发现机制,在 META-INF/services 目录下配置接口实现类,运行时通过 ServiceLoader 动态加载。

详细解析:

  • 核心流程:定义接口 → 在 META-INF/services/ 下创建以接口全限定名命名的文件 → 文件内容为实现类全限定名 → ServiceLoader.load(接口.class) 加载所有实现。
  • 与 API 的区别:API 是调用方依赖接口和实现(正向),SPI 是接口方定义规范、实现方按需插入(反向控制)。
  • 应用场景:JDBC 驱动(mysql-connector)、日志门面 SLF4J、Dubbo SPI(增强版,支持 IoC 和 AOP)、Spring Boot 自动配置(spring.factories)。
  • ServiceLoader 缺点:一次性加载所有实现、无法按需获取、无依赖注入。Dubbo SPI 做了增强:按 key 加载、支持自适应扩展、支持包装类。

生产场景: 系统需要支持多种支付渠道(微信、支付宝),且希望做到新增渠道不改代码。解决方法: 定义 PaymentService 接口,各渠道实现类配置在 META-INF/services 下,运行时 ServiceLoader 动态加载,策略模式按类型选择实现。

二、并发编程 JUC(Q11-Q25)

Q11:线程的生命周期和状态有哪些?

核心原理: Java 线程有 6 种状态(Thread.State 枚举):NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

详细解析:

  • NEW:创建了 Thread 对象但未调用 start()。
  • RUNNABLE:调用了 start(),可能正在执行也可能等待 CPU 调度(Java 将操作系统层的就绪和运行合并为 RUNNABLE)。
  • BLOCKED:等待获取 synchronized 监视器锁(如 synchronized 代码块/方法)。
  • WAITING:调用 Object.wait()Thread.join()LockSupport.park() 进入无限期等待,需被其他线程显式唤醒。
  • TIMED_WAITING:调用 Thread.sleep(ms)Object.wait(ms)Thread.join(ms) 进入限时等待,超时自动唤醒。
  • TERMINATED:线程执行完毕或异常退出。
  • 注意:调用 LockSupport.park() 进入的是 WAITING 而非 BLOCKED;ReentrantLock 等待锁的状态也是 WAITING(基于 AQS 的 LockSupport.park),而非 BLOCKED。

生产场景: 线上线程池中线程大量处于 BLOCKED 状态,导致任务积压。解决方法: jstack PID 导出线程堆栈,定位锁竞争点;缩短 synchronized 代码块范围,或改用 ReentrantLock 的 tryLock(timeout) 避免无限等待。

Q12:synchronized 的底层原理和锁升级过程?

核心原理: synchronized 基于 Monitor 对象实现(monitorenter/monitorexit 字节码指令),JDK 1.6 后引入锁升级优化:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

详细解析:

  • Monitor 机制:每个对象关联一个 Monitor(ObjectMonitor),包含 Owner(持有线程)、EntryList(阻塞队列)、WaitSet(等待队列)。monitorenter 获取 Monitor,monitorexit 释放。
  • 对象头 Mark Word:存储锁状态、线程 ID、hashcode、GC 年龄等信息,64 位结构。
  • 偏向锁:第一个线程获取锁时,Mark Word 记录线程 ID,后续同一线程进入无需 CAS(只需比对线程 ID)。适合单线程反复进入同步块的场景。JDK 15 后默认禁用偏向锁(维护成本高)。
  • 轻量级锁:出现竞争时,撤销偏向锁,线程通过 CAS 自旋获取锁(修改 Mark Word 指向栈中 Lock Record)。适合持有时间短、竞争不激烈的场景。
  • 重量级锁:自旋失败(超过阈值)后升级,通过操作系统互斥量(Mutex)实现,线程阻塞/唤醒需用户态-内核态切换,开销大。
  • 锁升级是单向的,不可降级。

生产场景: 高并发下 synchronized 导致线程大量阻塞,系统吞吐量下降。解决方法: 减小锁粒度(ConcurrentHashMap 分段锁/CAS);缩短临界区;读多写少用读写锁 ReentrantReadWriteLock;超高并发用 StampedLock(乐观读)。

Q13:volatile 关键字的原理和作用?

核心原理: volatile 保证可见性(修改后立即刷新到主内存,其他线程立即看到)和有序性(禁止指令重排序),但不保证原子性

详细解析:

  • 可见性:CPU 缓存导致每个线程有自己的工作内存副本。volatile 变量的写操作会触发缓存行失效(MESI 协议),其他线程读时从主内存重新加载。底层通过 x86 的 lock 前缀指令实现(锁定缓存行或总线锁)。
  • 有序性(内存屏障):volatile 写前插入 StoreStore 屏障(禁止前面的普通写与 volatile 写重排),写后插入 StoreLoad 屏障;volatile 读前插入 LoadLoad 屏障,读后插入 LoadStore 屏障。
  • 不保证原子性volatile int i; i++ 在多线程下仍不安全(i++ 是读-改-写三步操作,可能被中断)。需用 AtomicIntegersynchronized
  • 应用场景:双重检查单例(DCL)中 instance 必须加 volatile,防止指令重排导致返回未初始化的对象(new 对象分为:分配内存 → 初始化 → 赋值引用,重排后可能先赋值再初始化)。

生产场景: 线程间状态标志位 private static boolean running = true,另一线程修改为 false 后工作线程不停止。解决方法: 标志位加 volatile;或用 AtomicBoolean;更推荐用 Thread.interrupt() + isInterrupted() 实现优雅停止。

Q14:synchronized 和 ReentrantLock 的区别?

核心原理: synchronized 是 JVM 层面的关键字(基于 Monitor),ReentrantLock 是 JUC 层面的 API(基于 AQS)。

详细解析:

对比维度 synchronized ReentrantLock
实现层面 JVM 关键字,monitorenter/monitorexit JUC 类,基于 AQS
锁释放 自动释放(出代码块/方法) 手动 unlock(),必须在 finally 中
可中断 不可中断(等锁时不响应 interrupt) 可中断 lockInterruptibly()
超时获取 不支持 支持 tryLock(timeout)
公平锁 非公平 支持公平/非公平(构造参数)
条件变量 一个 waitSet(wait/notify) 多个 Condition(await/signal)
锁绑定 对象/类 Lock 对象
性能 JDK 6 后优化接近 ReentrantLock 高并发下略优
  • ReentrantLock 基于 AQS:state 表示重入次数,CLH 队列管理等待线程。公平锁先检查队列有无前驱,非公平锁直接 CAS 抢锁。
  • ReentrantLock 可绑定多个 Condition,实现精确唤醒(如生产者-消费者分别唤醒)。

生产场景: 需要实现"尝试获取锁 3 秒,超时走降级逻辑"。解决方法: 用 ReentrantLock 的 tryLock(3, TimeUnit.SECONDS),synchronized 无法实现超时获取。但简单场景仍优先 synchronized(代码简洁、不会忘记释放锁)。

Q15:AQS(AbstractQueuedSynchronizer)的原理?

核心原理: AQS 是 JUC 同步工具的基础框架,核心是 state 变量 + CLH 双向等待队列,通过模板方法模式让子类实现独占/共享获取释放逻辑。

详细解析:

  • state:volatile int,不同子类赋予不同含义。ReentrantLock 中表示重入次数;Semaphore 中表示许可数;CountDownLatch 中表示剩余计数。
  • CLH 队列:双向链表,存放等待获取锁的线程封装的 Node 节点。线程获取锁失败后封装为 Node 加入队尾,然后调用 LockSupport.park() 挂起;前驱节点释放后调用 LockSupport.unpark() 唤醒后继。
  • 独占模式tryAcquire()/tryRelease(),同一时刻只有一个线程获取同步状态(ReentrantLock)。
  • 共享模式tryAcquireShared()/tryReleaseShared(),允许多个线程同时获取(Semaphore、CountDownLatch、ReadWriteLock 的读锁)。
  • 公平 vs 非公平:公平锁 tryAcquire 先检查队列是否有前驱节点(hasQueuedPredecessors),非公平锁直接 CAS 抢锁。

生产场景: 自定义限流器,限制同时最多 N 个线程访问某资源。解决方法: 继承 AQS 实现共享模式,state 初始化为 N,tryAcquireShared 时 CAS 减 1(< 0 入队等待),tryReleaseShared 时 CAS 加 1。实际可直接用 Semaphore(N)。

Q16:ThreadLocal 的原理和内存泄漏问题?

核心原理: ThreadLocal 为每个线程提供变量副本,核心结构是 Thread 对象中的 ThreadLocalMap,key 为 ThreadLocal 对象(弱引用),value 为线程私有数据。

详细解析:

  • 数据结构:每个 Thread 对象有一个 ThreadLocal.ThreadLocalMap 成员。ThreadLocalMap 内部是 Entry[] 数组,Entry 继承 WeakReference,即 key 是弱引用。
  • set/get 流程:set 时以当前 ThreadLocal 为 key,存入当前线程的 ThreadLocalMap;get 时从当前线程的 Map 中取对应 ThreadLocal 的 value。
  • 内存泄漏根因:key 是弱引用,GC 后 key 变为 null,但 value 是强引用,Entry 对象仍被 ThreadLocalMap 引用。如果线程不结束(如线程池中的线程),这些 null-key 的 value 永远无法回收。
  • Hash 冲突:ThreadLocalMap 用开放寻址法(线性探测),不是拉链法。

生产场景: 线程池中使用 ThreadLocal 存储用户上下文(UserContext),线程复用后上一个用户的上下文未清理,导致数据串号。解决方法: 每次使用后在 finally 中调用 threadLocal.remove() 清理;或用阿里 TransmittableThreadLocal 解决线程池中 ThreadLocal 传递问题。InheritableThreadLocal 只能在创建子线程时传递,线程池复用场景无效。

Q17:线程池的核心参数和工作流程?

核心原理: ThreadPoolExecutor 有 7 个核心参数,通过核心线程数、队列、最大线程数、拒绝策略的配合控制任务执行。

详细解析:

  • 7 大参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程空闲存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。
  • 工作流程
    1. 提交任务,如果当前线程数 < corePoolSize,创建核心线程执行任务。
    2. 如果线程数 >= corePoolSize,任务放入 workQueue 排队。
    3. 如果队列已满且线程数 < maximumPoolSize,创建非核心线程执行任务。
    4. 如果队列满且线程数 >= maximumPoolSize,触发拒绝策略。
    5. 非核心线程空闲超过 keepAliveTime 后被回收。
  • 4 种拒绝策略:AbortPolicy(默认,抛 RejectedExecutionException)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃队列最老任务,重试提交)。
  • ** Executors 禁用原因**:newFixedThreadPoolnewSingleThreadExecutor 用 LinkedBlockingQueue(无界队列),可能 OOM;newCachedThreadPool 最大线程数为 Integer.MAX_VALUE,可能创建大量线程 OOM。阿里规范要求手动创建 ThreadPoolExecutor。

生产场景: 线程池队列使用无界队列,大促期间任务堆积导致 OOM。解决方法: 使用有界队列(如 ArrayBlockingQueue(1000));CPU 密集型任务 corePoolSize = N+1,IO 密集型 = 2N 或 N×(1+等待时间/计算时间);不同业务隔离线程池避免互相影响。

Q18:CAS 原理及 ABA 问题如何解决?

核心原理: CAS(Compare And Swap)是无锁乐观机制,包含三个操作数:内存值 V、期望值 A、新值 B。当 V==A 时将 V 更新为 B,否则不操作,整个操作是原子的(CPU 的 cmpxchg 指令)。

详细解析:

  • Unsafe 类:Java 通过 sun.misc.Unsafe 提供 CAS 能力,compareAndSwapInt(Object, offset, expect, update) 直接操作内存偏移量。
  • Atomic 类:AtomicInteger、AtomicReference 等基于 CAS 实现无锁原子操作,getAndIncrement() 内部是 do-while 自旋 CAS。
  • ABA 问题:线程 1 读取值 A,线程 2 将 A 改为 B 再改回 A,线程 1 的 CAS 仍然成功,但值实际已被修改过。对基本类型无影响,但对象引用场景可能导致数据不一致。
  • 解决方案AtomicStampedReference 每次修改附带版本号(A→B→A 变为 A1→B2→A3),CAS 时同时比较值和版本号;AtomicMarkableReference 用 boolean 标记。

生产场景: 库存扣减场景用 CAS compareAndSet(oldStock, newStock),高并发下 CAS 自旋失败率高(大量空转浪费 CPU)。解决方法: 使用 LongAdder 分段 CAS(base + Cell 数组分散竞争),高并发计数性能优于 AtomicLong;或用 Redis Lua 脚本做原子扣减。

Q19:CountDownLatch 和 CyclicBarrier 的区别?

核心原理: CountDownLatch 是一次性计数器(减到 0 后不可重置),CyclicBarrier 是可循环使用的屏障(所有线程到达后放行,可重置)。

详细解析:

  • CountDownLatch:基于 AQS 共享模式,count 表示剩余计数。countDown() 将 count 减 1,await() 在 count=0 前阻塞。一个线程等待 N 个线程完成(主线程等子任务),或 N 个线程同时开始。
  • CyclicBarrier:基于 ReentrantLock + Condition,parties 表示需要的线程数。await() 到达屏障后阻塞,最后一个线程到达时全部放行,并可执行 barrierAction。可调用 reset() 重置复用。
  • 核心区别:CountDownLatch 一次性不可复用;CyclicBarrier 可复用且支持到达后执行回调。CountDownLatch 是"一个人等多个人";CyclicBarrier 是"多个人互相等"。

生产场景: 多线程并行查询多个数据源,主线程等待所有查询完成后合并结果。解决方法:CountDownLatch(N),每个子线程查询完后 countDown(),主线程 await() 等待全部完成。如果是分批处理(每批 N 个线程),用 CyclicBarrier(N, () -> 合并结果)。

Q20:Java 中的锁分类有哪些?

核心原理: 按不同维度可分为:公平/非公平、可重入/不可重入、乐观/悲观、独占/共享、自旋/阻塞。

详细解析:

  • 公平锁 vs 非公平锁:公平锁按请求顺序获取(先到先得),非公平锁可插队。ReentrantLock 支持两种模式,synchronized 是非公平锁。非公平锁吞吐量更高(减少线程切换)。
  • 可重入锁:同一线程可多次获取同一把锁(state 递增计数),避免死锁。ReentrantLock 和 synchronized 都是可重入的。
  • 乐观锁 vs 悲观锁:悲观锁先加锁再操作(synchronized、ReentrantLock);乐观锁先操作再 CAS 验证(Atomic 类、数据库 version 字段)。乐观锁适合读多写少、冲突少。
  • 独占锁 vs 共享锁:独占锁同一时刻一个线程持有(ReentrantLock、synchronized);共享锁允许多线程同时持有(ReadWriteLock 的读锁、Semaphore)。
  • 自旋锁:获取锁失败时不阻塞,而是循环重试(CAS 自旋),适合持有时间极短的场景。但自旋过多浪费 CPU。

生产场景: 配置中心更新配置后通知所有节点刷新本地缓存。解决方法: 用读写锁 ReentrantReadWriteLock,读操作获取读锁(多线程并发读),配置更新获取写锁(互斥写),兼顾读并发和写安全。

Q21:死锁产生的条件和排查方法?

核心原理: 死锁是两个或多个线程互相等待对方持有的锁,导致永久阻塞。产生死锁需同时满足四个条件。

详细解析:

  • 四个必要条件:① 互斥(资源同一时刻只能一个线程使用);② 持有并等待(持有锁的同时等待其他锁);③ 不可剥夺(不能强行夺走锁);④ 循环等待(线程间形成环形等待链)。
  • 排查方法
    1. jstack PID 导出线程堆栈,查找 “Found one Java-level deadlock” 字段。
    2. jconsole / Arthas 可视化查看线程锁状态。
    3. 日志中线程长期处于 BLOCKED 状态且等待的锁被另一个 BLOCKED 线程持有。
  • 预防方法:① 统一加锁顺序(如按 ID 升序加锁,破坏循环等待);② 缩短锁持有时间;③ 使用 tryLock(timeout) 替代无限等待;④ 减小锁粒度。

生产场景: 转账系统中 A→B 和 B→A 同时执行,A 锁了账户 A 等 B 锁,B 锁了账户 B 等 A 锁,死锁。解决方法: 统一按账户 ID 从小到大的顺序加锁(if (fromId < toId) { lock(from); lock(to); } else { lock(to); lock(from); }),破坏循环等待条件。

Q22:CompletableFuture 的使用场景和原理?

核心原理: CompletableFuture 是 Java 8 引入的异步编程工具,支持链式调用、组合多个异步任务、异常处理,基于 ForkJoinPool.commonPool() 执行。

详细解析:

  • 创建supplyAsync(Supplier) 异步执行有返回值;runAsync(Runnable) 异步执行无返回值。默认使用 ForkJoinPool.commonPool(),可传入自定义 Executor。
  • 链式转换thenApply(Function) 转换结果;thenAccept(Consumer) 消费结果无返回值;thenRun(Runnable) 执行后续动作。
  • 组合thenCompose(Function) 串行依赖(前一个的结果传入后一个);allOf(cf1, cf2, cf3).join() 等待全部完成;anyOf(cf1, cf2).join() 任一完成即返回。
  • 异常处理exceptionally(Function) 捕获异常返回默认值;handle(BiFunction) 同时处理正常和异常结果。
  • 回调线程:then 系列方法默认在前一个任务执行的线程中回调,可指定 Async 后缀使用线程池异步回调。

生产场景: 商品详情页需并行查询商品基础信息、价格、库存、评价四个接口,合并后返回。解决方法:

CompletableFuture<BaseInfo> f1 = CompletableFuture.supplyAsync(() -> queryBase(id), executor);
CompletableFuture<Price> f2 = CompletableFuture.supplyAsync(() -> queryPrice(id), executor);
CompletableFuture<Stock> f3 = CompletableFuture.supplyAsync(() -> queryStock(id), executor);
CompletableFuture.allOf(f1, f2, f3).join(); // 并行等待
ProductDetail detail = new ProductDetail(f1.get(), f2.get(), f3.get());

接口耗时从串行的 300ms 降至并行的 100ms。

Q23:ForkJoinPool 的原理和应用场景?

核心原理: ForkJoinPool 采用工作窃取算法(Work-Stealing),每个工作线程有自己的双端队列,空闲时从其他线程队列尾部窃取任务,提高 CPU 利用率。

详细解析:

  • Fork/Join 模式:大任务 fork 拆分为子任务并行执行,子任务结果 join 合并。类似分治法。
  • 工作窃取:每个线程维护一个双端队列(Deque),自己 push/pop 任务从队头操作(LIFO,缓存友好),窃取线程从其他线程队尾窃取(FIFO,减少竞争)。
  • 与普通线程池区别:普通线程池所有线程共享一个队列(竞争大);ForkJoinPool 每个线程独立队列 + 窃取机制。
  • 应用场景:大数组并行计算(parallelSort)、流式计算 parallelStream(默认用 ForkJoinPool.commonPool())、递归分治任务。

生产场景: 批量处理 100 万条数据,需要并行且支持任务拆分。解决方法: 继承 RecursiveTask,定义阈值(如每 10000 条一组),fork 子任务处理,join 合并结果。注意 parallelStream 共用 commonPool,CPU 密集型任务会抢占资源,建议传自定义 ForkJoinPool。

Q24:Java 内存模型(JMM)的三大特性?

核心原理: JMM 定义了线程间共享变量的访问规则,核心是可见性、原子性、有序性三大特性。

详细解析:

  • 可见性:一个线程修改了共享变量,其他线程能立即看到。volatilesynchronized(解锁前刷新到主内存)、final(初始化完成后对其他线程可见)保证可见性。
  • 原子性:一个或多个操作不可被中断(要么全执行要么全不执行)。synchronizedLockAtomic 类保证原子性。基本类型赋值(除 long/double 外)天然原子。
  • 有序性:程序执行顺序符合预期(防止指令重排)。volatile(内存屏障禁止重排)、synchronized( Happens-Before 规则)保证有序性。
  • Happens-Before 规则:① 程序顺序规则(同一线程内前操作 happens-before 后操作);② 锁规则(unlock happens-before 后续 lock);③ volatile 规则(写 happens-before 后续读);④ 传递性;⑤ 线程启动规则(start() happens-before 线程内动作);⑥ 线程终止规则。

生产场景: 双重检查单例模式不加 volatile 导致获取到未初始化的对象。解决方法: instance 字段加 volatile,利用 volatile 的写后 StoreLoad 屏障,保证 new 对象的"分配内存→初始化→赋值引用"不被重排为"分配内存→赋值引用→初始化"。

Q25:如何排查线上 CPU 100% 的问题?

核心原理: CPU 100% 通常由死循环、频繁 Full GC、大量线程竞争、加密/序列化计算密集型操作引起。

详细排查流程:

  1. top 命令找到 CPU 最高的 Java 进程 PID。
  2. top -Hp PID 找到该进程中 CPU 最高的线程 TID(十进制)。
  3. printf "%x\n" TID 将线程 ID 转为十六进制(nid)。
  4. jstack PID | grep nid -A 30 查看该线程堆栈,定位到具体代码行。
  5. 如果是 GC 线程 CPU 高,用 jstat -gc PID 1000 查看 GC 频率,jmap -dump 分析内存。
  6. Arthas 工具:thread -n 3 直接查看 CPU 占比最高的 3 个线程堆栈。

常见原因及解决:

  • 死循环/空转:while(true) 中缺少 sleep 或退出条件 → 修复代码逻辑。
  • 频繁 Full GC:内存泄漏导致老年代频繁满 → jmap -histo:live 分析大对象,MAT 分析堆转储。
  • 锁竞争:大量线程 BLOCKED 自旋 → 缩小锁粒度,改用 CAS。
  • 正则回溯:恶意输入导致正则引擎指数级回溯 → 限制输入长度,用优化过的正则。

生产场景: 线上接口偶发 CPU 飙升到 100%,持续 30 秒后恢复。解决方法: Arthas profiler start 采集火焰图,定位到 JSON 序列化大对象时 CPU 飙升,改用更高效的序列化库(如 Jackson 替代 Gson)并限制序列化深度。

三、JVM 虚拟机(Q26-Q37)

Q26:JVM 内存区域划分(JDK 1.8)?

核心原理: JDK 1.8 内存分为:堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器。堆和方法区是线程共享的,其余是线程私有的。

详细解析:

  • 程序计数器(PC Register):记录当前线程执行的字节码行号,线程私有,唯一不会 OOM 的区域。
  • 虚拟机栈:线程私有,每个方法调用创建栈帧(局部变量表、操作数栈、动态链接、方法出口)。局部变量表存放基本类型和对象引用。StackOverflowError(栈深度超限)或 OOM(无法扩展)。
  • 本地方法栈:为 Native 方法服务,HotSpot 与虚拟机栈合二为一。
  • :最大内存区域,存放对象实例和数组。分为新生代(Eden + 2 个 Survivor,8:1:1)和老年代。所有线程共享。是 GC 主战场。
  • 方法区(元空间 Metaspace):JDK 1.8 用本地内存替代永久代,存放类元信息、常量池、静态变量。元空间使用本地内存,默认无上限(可设 -XX:MaxMetaspaceSize)。解决永久代容易 OOM 的问题。
  • 直接内存:NIO 的 DirectByteBuffer 使用堆外内存,不受 JVM 堆大小限制,但受操作系统内存限制。

生产场景: 元空间溢出 java.lang.OutOfMemoryError: Metaspace,因大量动态生成类(CGLIB 代理、Groovy 脚本)。解决方法: 设置 -XX:MaxMetaspaceSize=256m;检查是否有类泄漏(如自定义 ClassLoader 未释放)。

Q27:对象创建的过程?

核心原理: new 关键字创建对象经过:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造方法 <init>

详细解析:

  1. 类加载检查:检查常量池中类的符号引用是否已加载、解析、初始化,未加载则先执行类加载。
  2. 分配内存:在堆中划分内存。指针碰撞(内存规整,Serial/ParNew 收集器);空闲列表(内存碎片,CMS 收集器)。并发安全:CAS + 失败重试TLAB(Thread Local Allocation Buffer) 每个线程预分配一小块内存。
  3. 初始化零值:将分配的内存空间初始化为零值(int 为 0,引用为 null),使对象实例字段无需赋初值即可使用。
  4. 设置对象头:设置 Mark Word(hashcode、GC 分代年龄、锁状态)和类型指针(指向类元数据)。
  5. 执行 <init>:执行构造方法,赋值实例字段,完成对象创建。

生产场景: 高频创建对象导致 Minor GC 频繁。解决方法: 使用对象池复用对象(如 Netty 的 ByteBuf 池化);适当增大新生代大小 -Xmn;考虑 TLAB 调优 -XX:PretenureSizeThreshold 让大对象直接进老年代。

Q28:类加载机制和双亲委派模型?

核心原理: 类加载分为加载、验证、准备、解析、初始化五个阶段。双亲委派模型要求类加载请求先委托给父加载器,父加载器无法加载时才自己加载。

详细解析:

  • 类加载阶段
    • 加载:通过类全限定名获取字节流,生成 Class 对象。
    • 验证:文件格式、元数据、字节码、符号引用验证。
    • 准备:为静态变量分配内存并赋零值(static int a = 1 此阶段 a=0,初始化阶段才赋 1)。
    • 解析:符号引用替换为直接引用。
    • 初始化:执行 <clinit> 方法(静态变量赋值 + static 块)。
  • 双亲委派模型
    • 启动类加载器(Bootstrap ClassLoader):加载 JAVA_HOME/lib(rt.jar)。
    • 扩展类加载器(Extension ClassLoader):加载 JAVA_HOME/lib/ext
    • 应用类加载器(Application ClassLoader):加载 classpath。
    • 自定义类加载器:继承 ClassLoader 重写 findClass()。
  • 工作流程:收到加载请求 → 委托父加载器 → 父加载器再向上委托 → 直到 Bootstrap → 如果 Bootstrap 无法加载则向下返回,由子加载器尝试。
  • 意义:保证核心类(如 java.lang.Object)不会被自定义类替换(安全);避免重复加载。

生产场景: Tomcat 打破了双亲委派模型——每个 Web 应用的类加载器优先加载自己的类(隔离不同应用的类)。解决方法: Tomcat 的 WebappClassLoader 先自己加载(findClass),找不到再委托父加载器,实现应用间类隔离。SPI 机制(如 JDBC DriverManager)用线程上下文类加载器(TCCL)解决父加载器无法看到子加载器类的问题。

Q29:JVM 垃圾回收算法有哪些?

核心原理: 垃圾回收核心是判断对象是否存活(可达性分析)和回收策略(标记-清除、标记-复制、标记-整理)。

详细解析:

  • 判断存活
    • 引用计数法(已淘汰,无法解决循环引用)。
    • 可达性分析:从 GC Roots(栈中引用、静态变量、常量、JNI 引用)出发遍历对象图,不可达的对象为垃圾。
  • 标记-清除(Mark-Sweep):标记所有垃圾对象,然后清除。缺点:产生内存碎片,分配大对象时可能触发又一次 GC。
  • 标记-复制(Copying):将存活对象复制到另一块区域,清空原区域。缺点:浪费一半内存。适合新生代(存活对象少)。
  • 标记-整理(Mark-Compact):标记存活对象后,向一端移动整理。无碎片但效率低(需移动对象)。适合老年代。
  • 分代收集:新生代用复制算法(Eden + 2 Survivor,存活率低),老年代用标记-清除或标记-整理(存活率高)。
  • 分区算法:G1 将堆分为多个 Region,每个 Region 独立回收,控制停顿时间。

生产场景: CMS 收集器用标记-清除产生大量碎片,导致晋升失败触发 Full GC(Serial Old 单线程,停顿极长)。解决方法: 设置 -XX:CMSFullGCsBeforeCompaction=5 每 5 次 Full GC 后做一次内存整理;或升级到 G1/ZGC。

Q30:常见的垃圾收集器及其特点?

核心原理: 垃圾收集器按分代和并发性分为:Serial、ParNew、Parallel Scavenge、CMS、G1、ZGC、Shenandoah。

详细解析:

收集器 分代 算法 并发 特点
Serial / Serial Old 新生代/老年代 复制/整理 单线程 简单,适合客户端
ParNew 新生代 复制 多线程并行 Serial 多线程版,配合 CMS
Parallel Scavenge / Old 新生代/老年代 复制/整理 多线程并行 注重吞吐量
CMS 老年代 标记-清除 并发 低停顿,4 阶段:初始标记(STW)→并发标记→重新标记(STW)→并发清除
G1 全堆(分区) 复制+整理 并发 可预测停顿,Region 分区,适合大堆
ZGC 全堆 染色指针+读屏障 并发 停顿 < 10ms,支持 TB 级堆
Shenandoah 全堆 转发指针 并发 停顿与堆大小无关
  • CMS 四阶段:初始标记(STW,标记 GC Roots 直接引用)→ 并发标记(GC 线程与用户线程并发,遍历对象图)→ 重新标记(STW,修正并发标记期间引用变化)→ 并发清除(并发清除垃圾)。
  • G1 特点:堆分为 2048 个 Region(1-32MB),每个 Region 可动态切换为 Eden/Survivor/Old/Humongous。维护 Remembered Set 记录跨 Region 引用。-XX:MaxGCPauseMillis=200 设定目标停顿时间,G1 根据回收价值优先回收收益最大的 Region。
  • ZGC 特点:染色指针(64 位指针中借几位标记状态),读屏障(读取引用时自动转发),所有阶段都并发(仅初始标记和再标记极短 STW)。

生产场景: 8GB 堆用 CMS 导致频繁 Full GC 且停顿过长(>1秒)。解决方法: 升级到 G1,-XX:+UseG1GC -XX:MaxGCPauseMillis=200,G1 在大堆下停顿可控。JDK 15+ 生产可用 ZGC 实现亚 10ms 停顿。

Q31:Full GC 触发的原因和排查方法?

核心原理: Full GC 是对整个堆(新生代+老年代+元空间)进行全面回收,停顿时间长,应尽量避免。

详细排查流程:

  • 常见触发原因
    1. 老年代空间不足:大量对象晋升到老年代(大对象直接进入、Survivor 不够晋升、年龄阈值达到)。
    2. 元空间不足:动态生成类过多,Metaspace 溢出触发 Full GC。
    3. System.gc():代码显式调用(建议 -XX:+DisableExplicitGC 禁用)。
    4. CMS 并发模式失败(Concurrent Mode Failure):CMS 并发回收期间老年代满了,退化为 Serial Old 单线程 Full GC。
    5. 空间分配担保失败:Minor GC 前检查老年代连续空间是否够容纳所有新生代对象,不够则触发 Full GC。
    6. promotion failed:Minor GC 时 Survivor 放不下要晋升的对象,老年代也没有连续空间。
  • 排查步骤
    1. jstat -gc PID 1000 10 查看 FGC(Full GC 次数)和 FGCT(总耗时)。
    2. GC 日志加 -Xlog:gc*(JDK 9+)或 -XX:+PrintGCDetails
    3. jmap -histo:live PID 查看存活对象分布。
    4. jmap -dump:format=b,file=heap.hprof PID 导出堆转储,用 MAT 分析。
  • 解决方法:内存泄漏 → 修复代码(如静态集合未清理);大对象 → 调 -XX:PretenureSizeThreshold;老年代太小 → 调 -XX:NewRatio;CMS 碎片 → 定期整理或换 G1。

生产场景: 系统 FGC 每 5 分钟一次,每次 3 秒。jmap 发现 static List 持有大量用户数据未清理。解决方法: 改用 Guava Cache 设置 maximumSize + expireAfterWrite 自动清理,FGC 降至 1 小时一次。

Q32:Java 中的四种引用类型?

核心原理: Java 提供强、软、弱、虚四种引用,强度依次递减,影响 GC 回收行为。

详细解析:

  • 强引用(StrongReference)Object obj = new Object(),只要强引用存在就不回收。即使 OOM 也不回收强引用对象。
  • 软引用(SoftReference):内存不足时才回收。适合内存敏感的缓存。SoftReference<byte[]> cache = new SoftReference<>(new byte[1024*1024])
  • 弱引用(WeakReference):下一次 GC 时无论内存是否充足都回收。ThreadLocalMap 的 key 是弱引用;WeakHashMap 的 key 是弱引用。
  • 虚引用(PhantomReference):最弱引用,get() 永远返回 null,唯一作用是在对象被回收时收到通知(配合 ReferenceQueue)。用于跟踪对象回收时机,NIO DirectByteBuffer 用虚引用管理堆外内存释放。
  • 引用队列(ReferenceQueue):软/弱/虚引用关联的对象被回收后,引用对象本身会加入 ReferenceQueue,可用于清理操作。

生产场景: 缓存大对象(如图片)到内存,希望内存充足时命中缓存,内存不足时自动释放。解决方法:SoftReference 包装缓存对象,而非强引用,避免 OOM。或直接用 Guava/Caffeine 设置 weakValues()。

Q33:JVM 调优的常用参数有哪些?

核心原理: JVM 调优核心是堆大小、新生代/老年代比例、收集器选择、GC 日志。

常用参数:

  • 堆大小-Xms4g -Xmx4g(初始堆和最大堆设为相同,避免动态扩缩引起抖动)。
  • 新生代-Xmn2g(新生代大小)或 -XX:NewRatio=2(老年代:新生代=2:1)。
  • Survivor-XX:SurvivorRatio=8(Eden:S0:S1=8:1:1)。
  • 元空间-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
  • 收集器-XX:+UseG1GC(G1)、-XX:MaxGCPauseMillis=200(目标停顿)、-XX:+UseZGC(ZGC)。
  • GC 日志:JDK 9+ -Xlog:gc*:file=gc.log:time,uptime,level,tags;JDK 8 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
  • 堆 dump-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
  • 直接内存-XX:MaxDirectMemorySize=1g(NIO 堆外内存上限)。
  • 禁用 System.gc()-XX:+DisableExplicitGC
  • 大对象阈值-XX:PretenureSizeThreshold=10m(大于此值的对象直接进老年代,仅 Serial/ParNew 有效)。

生产场景: 4C8G 服务器部署 Java 服务,初步调优配置。解决方法: -Xms4g -Xmx4g -Xmn2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/logs/heap.hprof -Xlog:gc*:file=/opt/logs/gc.log。生产环境 Xms 和 Xmx 设为相同值避免动态扩容抖动。

Q34:对象逃逸分析与栈上分配?

核心原理: 逃逸分析是 JIT 编译器分析对象的动态作用域,判断对象是否逃逸出方法或线程。未逃逸的对象可在栈上分配,避免堆 GC。

详细解析:

  • 逃逸类型
    • 方法逃逸:对象被方法外引用(如返回值、赋值给静态变量、传给其他方法)。逃逸后对象需在堆上分配。
    • 线程逃逸:对象被其他线程访问(如赋值给类成员变量、被其他线程读取)。
    • 未逃逸:对象仅在方法内部使用,可优化。
  • 优化手段
    • 栈上分配:未逃逸的对象在栈帧上分配,方法结束自动释放,无需 GC。
    • 标量替换:将对象拆解为基本类型(标量),直接用局部变量替代,减少对象分配。
    • 同步消除:未线程逃逸的对象,其同步操作可被消除(无竞争)。
  • -XX:+DoEscapeAnalysis 开启逃逸分析(JDK 8 默认开启)。

生产场景: 方法内创建大量临时对象(如循环内 new DTO),导致 Minor GC 频繁。解决方法: 确保对象不逃逸(不返回、不赋值给成员变量),JIT 会自动栈上分配。也可手动复用对象。验证:-XX:+PrintEscapeAnalysis 查看分析结果。

Q35:JIT 即时编译器原理?

核心原理: JIT(Just-In-Time)编译器在运行时将热点代码的字节码编译为本地机器码,提升执行性能。HotSpot 有 C1(Client)和 C2(Server)两个编译器。

详细解析:

  • 热点探测:基于计数器判断是否为热点代码。方法调用计数器 + 回边计数器(循环),超过阈值(-XX:CompileThreshold=10000)触发编译。
  • C1 编译器:快速编译,简单优化(内联、常量折叠),适合客户端或启动速度优先的场景。
  • C2 编译器:激进优化(逃逸分析、锁消除、循环展开、虚方法内联),编译慢但运行快,适合服务端。
  • 分层编译(Tiered Compilation):JDK 8 默认开启,先用 C1 快速编译,热点代码再用 C2 重新编译。兼顾启动速度和峰值性能。
  • 常见优化
    • 方法内联:将被调用方法体直接嵌入调用处,消除方法调用开销(最重要优化)。
    • 锁消除:逃逸分析发现对象未逃逸,消除 synchronized。
    • 循环展开:减少循环次数,减少分支判断。
  • AOT 编译:JDK 9+ 引入 jaotc,编译期生成机器码(GraalVM),启动快但无运行时 profile 优化。

生产场景: 微服务启动慢(JIT 预热),刚启动时接口响应慢。解决方法: JVM 预热(启动后用脚本发预热请求触发 JIT 编译);或 GraalVM AOT 编译为原生镜像(启动 <50ms),适合 Serverless 场景。

Q36:线上 OOM 如何排查?

核心原理: OOM(OutOfMemoryError)分为 Java 堆空间溢出、元空间溢出、GC 开销超限、直接内存溢出等类型,需根据类型定位。

详细排查流程:

  1. 确认 OOM 类型
    • Java heap space:堆内存不足。
    • Metaspace:元空间不足(类元数据过多)。
    • GC overhead limit exceeded:98% 时间在 GC 且回收不到 2% 内存。
    • Direct buffer memory:NIO 堆外内存不足。
  2. 获取堆转储:启动参数 -XX:+HeapDumpOnOutOfMemoryError 自动 dump,或 jmap -dump:format=b,file=heap.hprof PID
  3. MAT 分析
    • Histogram:按类统计对象数量和大小。
    • Dominator Tree:找占用内存最大的对象。
    • Leak Suspects:自动分析疑似泄漏点。
    • 查看对象的 GC Root 引用链,定位为何无法回收。
  4. 常见原因
    • 静态集合无限增长(static List/Map 未清理)。
    • ThreadLocal 未 remove(线程池场景)。
    • 大查询未分页(一次查百万数据到内存)。
    • 连接/流未关闭(数据库连接、IO 流泄漏)。
    • 无限递归(StackOverflowError 转化为 OOM)。

生产场景: 线上服务每 2 小时 OOM 重启一次。MAT 分析发现 HashMap 持有 200 万个 User 对象,是缓存未设上限。解决方法: 改用 Caffeine 缓存设置 maximumSize(10000) + expireAfterWrite(30min),OOM 不再出现。

Q37:如何选择垃圾收集器?

核心原理: 根据堆大小、停顿要求、吞吐量要求、JDK 版本选择合适的收集器。

选型指南:

  • 堆 < 4GB,停顿不敏感:Parallel Scavenge + Parallel Old(吞吐量优先,JDK 8 默认)。
  • 堆 4-8GB,低停顿:ParNew + CMS(JDK 8 常用,CMS 已在 JDK 14 移除)。
  • 堆 8GB+,可预测停顿:G1(JDK 9+ 默认,推荐)。
  • 堆 16GB+,超低停顿:ZGC(JDK 15+ 生产可用,停顿 <10ms)或 Shenandoah。
  • Serverless/容器:Serial GC(资源受限,-XX:+UseSerialGC)或 GraalVM 原生镜像。

关键考虑因素:

  • 停顿时间:CMS/G1/ZGC 并发收集,停顿短。
  • 吞吐量:Parallel Scavenge 最高,但停顿长。
  • 堆大小:G1 适合 8GB+,ZGC 适合 TB 级。
  • JDK 版本:CMS 在 JDK 14 移除,ZGC 在 JDK 15 转 GA。

生产场景: 从 JDK 8 升级到 JDK 17,8GB 堆。解决方法: 直接用默认的 G1(-XX:+UseG1GC),设置 -XX:MaxGCPauseMillis=200,G1 自动调整 Region 大小和回收策略。观察 GC 日志确认停顿在 200ms 以内。

四、MySQL 数据库(Q38-Q49)

Q38:InnoDB 和 MyISAM 的区别?

核心原理: InnoDB 支持事务、行锁、外键;MyISAM 不支持事务、仅表锁、无外键,但查询速度快。

详细解析:

特性 InnoDB MyISAM
事务 支持 ACID 不支持
锁粒度 行锁(默认)+ 表锁 仅表锁
外键 支持 不支持
索引结构 聚簇索引(数据和主键索引一起) 非聚簇索引(数据和索引分离)
全文索引 5.6+ 支持 支持
崩溃恢复 支持(redo log) 不支持(需修复表)
适用场景 OLTP(高并发读写) 只读或以读为主的场景
  • InnoDB 的行锁是基于索引实现的:如果查询未走索引会退化为表锁。
  • MySQL 5.5 后默认存储引擎为 InnoDB。MyISAM 在 MySQL 5.5 前是默认引擎,现在已逐渐被淘汰。

生产场景: 历史日志表只做写入和批量查询,无事务需求,MyISAM 查询更快。解决方法: MySQL 8.0+ 不建议用 MyISAM,InnoDB 的查询性能已大幅优化。日志表可用 InnoDB + 归档策略(按月分区),历史数据归档到专用表。

Q39:MySQL 索引数据结构为什么用 B+ 树?

核心原理: B+ 树是多路平衡查找树,非叶子节点只存索引不存数据(扇出大),所有数据在叶子节点(有序链表),适合磁盘 IO 和范围查询。

详细解析:

  • B+ 树 vs B 树:B 树每个节点都存数据,导致单个节点能放的索引键少(扇出小),树更高,IO 次数多。B+ 树非叶子节点只存索引,一个 16KB 页可放上千个索引键(扇出大),3-4 层即可存储千万级数据(每层 1000 倍:1000³ = 10 亿)。
  • B+ 树 vs 红黑树:红黑树是二叉树,树高 = log₂(N),1000 万数据约 24 层,即 24 次磁盘 IO。B+ 树 3-4 层仅需 3-4 次 IO。
  • B+ 树 vs 哈希:哈希 O(1) 等值查询快,但不支持范围查询、排序、最左前缀匹配。MySQL 的 Memory 引擎用哈希索引。
  • 叶子节点链表:B+ 树叶子节点通过双向链表连接,范围查询只需定位起点后顺序遍历,高效。
  • InnoDB 页大小:默认 16KB,innodb_page_size=16K。一个页放一个节点。
  • 聚簇索引:InnoDB 主键索引的叶子节点存储完整行数据(数据和索引一体)。辅助索引叶子节点存储主键值(需回表)。

生产场景: 百万级数据的订单表查询慢。解决方法: 确保查询条件走索引(B+ 树 3-4 次 IO 即可定位);避免 SELECT *(减少回表和 IO);联合索引按最左前缀设计。

Q40:聚簇索引和非聚簇索引的区别?

核心原理: 聚簇索引的叶子节点存储完整行数据(索引即数据),非聚簇索引的叶子节点存储主键值(需回表查询)。

详细解析:

  • 聚簇索引(Clustered Index):InnoDB 的主键索引。一张表只有一个聚簇索引。叶子节点 = 索引键 + 完整行数据。数据物理上按主键有序存储。如果没有主键,InnoDB 会选唯一非空索引,如果没有则生成隐藏的 ROWID 列。
  • 非聚簇索引(Secondary Index / 辅助索引):InnoDB 的非主键索引。叶子节点 = 索引键 + 主键值。查询时先在辅助索引找到主键值,再到聚簇索引查行数据(回表)。
  • 覆盖索引:如果查询的字段全部包含在索引中,无需回表。如 SELECT name, age FROM users WHERE name = '张三',如果 (name, age) 是联合索引则覆盖。
  • MyISAM:主键索引和辅助索引的叶子节点都存储行数据的物理地址(指针),都是非聚簇的。

生产场景: 高频查询 SELECT id, name FROM user WHERE name = '张三',name 上有单列索引但每次需回表。解决方法: 创建联合索引 idx_name_id (name, id),利用覆盖索引避免回表,查询性能提升数倍。

Q41:索引失效的常见场景有哪些?

核心原理: 索引失效是指查询条件中有索引列但 MySQL 未使用索引,改为全表扫描。

常见失效场景:

  1. 对索引列做函数/运算WHERE YEAR(create_time) = 2026 → 改为 WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01'
  2. 隐式类型转换WHERE phone = 13800138000(phone 是 VARCHAR)→ MySQL 将字符串转数字,索引失效 → 改为 WHERE phone = '13800138000'
  3. 左模糊查询WHERE name LIKE '%张三' → 索引失效。LIKE '张三%' 可走索引(最左匹配)。全模糊需用 ES。
  4. 联合索引非最左前缀:索引 (a, b, c)WHERE b = 1 AND c = 2 不走索引(跳过了 a)。
  5. OR 条件WHERE a = 1 OR b = 2,如果 b 无索引则整体不走索引。两边都有索引才可能走索引合并。
  6. NOT IN / NOT EXISTS / !=:通常导致全表扫描(取决于数据分布和 MySQL 版本优化)。
  7. 索引列使用 IS NULL:部分场景可能不走索引(取决于数据分布)。
  8. 字符集不一致:JOIN 时两张表字符集不同(utf8 vs utf8mb4),隐式转换导致索引失效。
  9. 优化器认为全表扫描更快:数据量小或索引区分度低时,MySQL 可能选择全表扫描。

排查方法EXPLAIN 查看 type(ALL 表示全表扫描)、key(实际使用的索引)、rows(预估扫描行数)、Extra(Using index 表示覆盖索引)。

生产场景: 接口慢查询告警,WHERE DATE(create_time) = '2026-07-03' 走全表扫描。解决方法: 改写为范围查询 WHERE create_time >= '2026-07-03 00:00:00' AND create_time < '2026-07-04 00:00:00',走索引扫描。

Q42:MySQL 事务隔离级别和 MVCC 原理?

核心原理: MySQL 有四种隔离级别(读未提交、读已提交、可重复读、串行化),InnoDB 默认可重复读(RR),通过 MVCC 实现。

详细解析:

  • 四种隔离级别
    • 读未提交(Read Uncommitted):脏读(读到未提交的数据)。
    • 读已提交(Read Committed):不可重复读(同一事务两次读结果不同)。Oracle/PG 默认。
    • 可重复读(Repeatable Read):幻读(同一事务两次范围查询结果不同)。InnoDB 默认。
    • 串行化(Serializable):完全串行,性能最差。
  • MVCC(多版本并发控制)
    • 每行记录隐藏字段:DB_TRX_ID(最后修改的事务 ID)、DB_ROLL_PTR(回滚指针,指向 undo log 中的历史版本)、DB_ROW_ID(行 ID)。
    • undo log 版本链:每次修改生成一条 undo log,通过回滚指针串联,形成版本链。
    • ReadView:事务执行快照读时生成 ReadView,包含:当前活跃事务 ID 列表、最小活跃事务 ID、下一个事务 ID、创建者事务 ID。
    • 可见性判断:遍历版本链,对每个版本判断 DB_TRX_ID 是否在 ReadView 的活跃事务列表中。如果不在(已提交)则可见;如果在(未提交)则不可见,继续往前找。
    • RC vs RR:RC 每次 SELECT 都生成新 ReadView(所以能看到最新提交的数据);RR 只在事务第一次 SELECT 时生成 ReadView(后续复用,所以可重复读)。
  • 当前读 vs 快照读:快照读(普通 SELECT)走 MVCC;当前读(SELECT FOR UPDATE、UPDATE、DELETE)读取最新已提交数据并加锁。

生产场景: 高并发下订单查询和更新冲突,RR 隔离级别下出现间隙锁导致死锁。解决方法: 理解 RR 下的 Next-Key Lock(记录锁 + 间隙锁),缩小事务范围;或降级到 RC 隔离级别(无间隙锁,但需处理不可重复读)。

Q43:MySQL 的锁机制(行锁、间隙锁、临键锁)?

核心原理: InnoDB 的锁分为记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock),用于在不同隔离级别下保证并发安全。

详细解析:

  • 记录锁(Record Lock):锁住索引上的一条记录。WHERE id = 1 FOR UPDATE 锁住 id=1 这一行。
  • 间隙锁(Gap Lock):锁住两条记录之间的间隙(不含记录本身),防止其他事务在间隙中插入新记录,解决幻读。如 id 有 [1, 5, 10],WHERE id > 1 AND id < 5 FOR UPDATE 锁住 (1, 5) 间隙,其他事务无法插入 id=2,3,4。
  • 临键锁(Next-Key Lock):记录锁 + 间隙锁,锁住一条记录及其前面的间隙。如锁住 id=5 的 Next-Key Lock = (1, 5]。这是 RR 隔离级别下的默认行锁类型。
  • 意向锁(Intention Lock):表级锁,IS(意向共享锁)/ IX(意向排他锁)。事务加行锁前先加表级意向锁,用于快速判断表中是否有行锁,避免全表扫描。
  • 插入意向锁:INSERT 操作在插入前设置,表示意图在某个间隙插入。如果间隙被 Gap Lock 锁住则等待。
  • RC 隔离级别:无间隙锁,只有记录锁。RR 隔离级别:有 Next-Key Lock 防幻读。
  • 锁降级:唯一索引等值查询且记录存在时,Next-Key Lock 降级为 Record Lock。

生产场景: 事务 A SELECT * FROM orders WHERE id > 100 FOR UPDATE 锁住 (100, +∞),事务 B 尝试 INSERT id=200 被阻塞。解决方法: 缩小锁定范围(明确条件 WHERE id = 101);高并发插入场景考虑用 RC 隔离级别(无间隙锁)。

Q44:MySQL 死锁如何分析和排查?

核心原理: 死锁是两个或多个事务互相等待对方持有的锁,InnoDB 有死锁检测机制(wait-for graph),检测到后回滚代价较小的事务。

排查方法:

  1. SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 段,记录了死锁时两个事务执行的 SQL 和持有的锁。
  2. 开启死锁日志:SET GLOBAL innodb_print_all_deadlocks = ON,死锁信息写入 error log。
  3. 分析死锁 SQL:确认事务加锁顺序、锁类型(记录锁/间隙锁/临键锁)。
  4. information_schema.INNODB_TRX 查看当前活跃事务。

常见死锁原因及解决:

  • 加锁顺序不一致:事务 A 先锁 id=1 再锁 id=2,事务 B 反向 → 统一加锁顺序。
  • 间隙锁冲突:RR 下两个事务分别用范围条件加锁,间隙重叠 → 缩小范围或用 RC。
  • 唯一索引冲突:两个事务同时 INSERT 相同唯一键,一个等待另一个的锁 → 业务层加分布式锁或先查后插。
  • ** UPDATE 锁升级**:UPDATE 未走索引导致全表扫描行锁 → 确保走索引。

生产场景: 高并发下两个事务交叉更新不同行的库存,偶发死锁。SHOW ENGINE INNODB STATUS 发现事务 A 锁了商品 1 等商品 2,事务 B 反向。解决方法: 业务层统一按商品 ID 排序后加锁;设置 innodb_lock_wait_timeout = 5(默认 50s)缩短锁等待;重试机制捕获 DeadlockLoserDataAccessException 自动重试。

Q45:MySQL 主从复制原理?

核心原理: MySQL 主从复制基于 binlog 日志,主库将变更写入 binlog,从库通过 IO 线程拉取并写入 relay log,SQL 线程重放 relay log 执行变更。

详细解析:

  • 三个线程
    • 主库 Dump 线程:读取 binlog 发送给从库 IO 线程。
    • 从库 IO 线程:接收 binlog 事件,写入 relay log。
    • 从库 SQL 线程:读取 relay log,重放 SQL 语句。
  • 复制方式
    • 异步复制(默认):主库提交事务后不等待从库确认,性能高但可能丢数据。
    • 半同步复制:主库提交后至少等待一个从库收到 binlog(写入 relay log)才返回,平衡性能和数据安全。
    • 全同步复制:等待所有从库执行完毕,性能差,很少用。
  • binlog 格式
    • STATEMENT:记录 SQL 语句,日志小但某些函数(NOW()、UUID())可能导致不一致。
    • ROW:记录行变更(前镜像+后镜像),日志大但一致性最好,推荐。
    • MIXED:混合模式,默认用 STATEMENT,不安全时用 ROW。
  • GTID 复制:全局事务 ID,每个事务有唯一 GTID,从库按 GTID 顺序复制,避免传统基于 binlog 位点复制的复杂性。

生产场景: 主从延迟导致读从库时数据不一致(用户刚下单查不到订单)。解决方法: MySQL 5.7+ 并行复制(slave_parallel_workers)多线程重放;写后读强制走主库(ShardingSphere 读写分离配置 forceRouteMaster);关键业务(支付)直连主库。

Q46:MySQL 慢查询如何优化?

核心原理: 慢查询优化的核心是分析执行计划、优化索引、改写 SQL、调整表结构。

优化步骤:

  1. 开启慢查询日志SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;
  2. 分析慢查询mysqldumpslow -s t -t 10 slow.log 或 pt-query-digest 按耗时排序 Top 10。
  3. EXPLAIN 分析执行计划
    • type:ALL(全表扫描,最差)→ index(全索引扫描)→ range(范围扫描)→ ref(非唯一索引等值)→ eq_ref(唯一索引等值)→ const(主键等值,最优)。
    • key:实际使用的索引,NULL 表示未走索引。
    • rows:预估扫描行数,越少越好。
    • Extra:Using index(覆盖索引好)、Using filesort(需优化)、Using temporary(需优化)。
  4. 索引优化:为 WHERE、JOIN、ORDER BY、GROUP BY 字段建索引;联合索引按区分度高的字段在前;避免冗余索引。
  5. SQL 改写:避免 SELECT *;大 IN 列表改为 JOIN;OR 改为 UNION ALL;子查询改为 JOIN;分页用游标 WHERE id > lastId LIMIT 10
  6. 表结构优化:大字段拆分到扩展表(垂直拆分);冷热数据分离(历史数据归档);适当反范式化(冗余字段避免 JOIN)。

生产场景: SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 100000, 10 耗时 3 秒。解决方法: 深度分页改用游标 WHERE user_id = 123 AND id < #{lastId} ORDER BY id DESC LIMIT 10;建联合索引 idx_user_id_create_time (user_id, create_time);避免 SELECT * 只查需要的列。

Q47:MySQL 深度分页如何优化?

核心原理: LIMIT offset, size 当 offset 很大时,MySQL 需扫描 offset+size 行再丢弃前 offset 行,效率极低。

优化方案:

  1. 游标分页(推荐)WHERE id > #{lastId} ORDER BY id ASC LIMIT 10,每次记录上一页最后一条 id,直接定位。O(1) 复杂度,但只能顺序翻页不能跳页。
  2. 子查询延迟关联SELECT * FROM orders o INNER JOIN (SELECT id FROM orders WHERE user_id = 1 ORDER BY create_time DESC LIMIT 100000, 10) t ON o.id = t.id。子查询走覆盖索引快速定位 id,再回表取数据,减少回表次数。
  3. ES search_after:亿级数据深度分页用 Elasticsearch 的 search_after,基于上一页排序值的游标分页。
  4. 禁止跳页:产品层面只允许"上一页/下一页",不允许直接跳到第 N 页。
  5. 冷热分离:历史数据归档到归档表/分区表,主表保持小体量。

生产场景: 订单列表 LIMIT 1000000, 10 耗时 5 秒。解决方法: 改为延迟关联 + 联合索引 (user_id, create_time, id),子查询 SELECT id FROM orders WHERE user_id = 1 ORDER BY create_time DESC LIMIT 1000000, 10 走覆盖索引只扫描索引不回表,再 JOIN 取数据,耗时降至 200ms。

Q48:MySQL 分库分表的方案和问题?

核心原理: 当单表数据量过大(通常 >1000 万或 >100GB),需通过垂直拆分(按字段)或水平拆分(按行)分散到多个库/表。

详细解析:

  • 垂直拆分
    • 垂直分表:将大字段(如 TEXT/BLOB)拆到扩展表,主表只留常用字段。
    • 垂直分库:按业务拆分(用户库、订单库、商品库),解决单库表过多、IO 竞争。
  • 水平拆分
    • 按 hash 取模user_id % N 分配到 N 个分片,数据均匀但扩容需重新迁移。
    • 按范围分片:如按时间(每月一个表),冷热分离但写入热点集中。
    • 一致性哈希:扩容时只迁移部分数据,减少迁移量。
  • 分片键选择:选查询频率最高、区分度高的字段(如 user_id)。非分片键查询需广播或建索引表。
  • 跨库问题
    • 跨库 JOIN:应用层组装、数据冗余、ES 异构。
    • 跨库事务:分布式事务(Seata AT / 本地消息表 / TCC)。
    • 跨库分页:各分片查 Top N 再归并排序,分片多时性能差。
    • 全局唯一 ID:Snowflake、Leaf-Segment。
  • 中间件:ShardingSphere(Sharding-JDBC / Sharding-Proxy)、MyCat。

生产场景: 日增 100 万订单,单表 5000 万后查询变慢。解决方法:order_id % 16 分 16 张表(4 库 × 4 表);订单 ID 嵌入时间基因(日期前缀 + 雪花 ID),按时间范围查询可定位分片;跨库查询异构到 ES;扩容用双写方案(新旧并行 → 灰度切流 → 旧库下线)。

Q49:MySQL 事务的 ACID 特性和实现原理?

核心原理: ACID 指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),InnoDB 通过 redo log、undo log、MVCC、锁机制实现。

详细解析:

  • 原子性:事务内操作要么全部成功要么全部回滚。实现:undo log(回滚日志),记录修改前的数据。事务执行过程中先写 undo log,回滚时根据 undo log 恢复。
  • 持久性:事务提交后数据永久保存。实现:redo log(重做日志),WAL(Write-Ahead Logging)机制——先写 redo log 再写磁盘数据页。redo log 是物理日志(记录"哪个页哪个偏移量改成什么"),顺序写入性能高。崩溃恢复时重放 redo log。
  • 隔离性:并发事务互不干扰。实现:MVCC + 锁。快照读用 MVCC(多版本),当前读用锁(行锁、间隙锁)。
  • 一致性:事务执行前后数据状态正确。由原子性 + 隔离性 + 持久性 + 应用层约束共同保证。
  • 两阶段提交(2PC):InnoDB 写 redo log prepare → MySQL 写 binlog → InnoDB 写 redo log commit。保证 binlog 和 redo log 一致,主从复制不丢数据。

生产场景: 事务过大导致锁持有时间长,并发性能差。解决方法: 缩小事务范围(只包含写操作,查询移到事务外);避免长事务(SET GLOBAL innodb_kill_idle_transaction 杀空闲事务);大事务拆分为小事务。

五、Redis 缓存(Q50-Q59)

Q50:Redis 常用数据类型及应用场景?

核心原理: Redis 有 5 种基本数据类型(String、List、Hash、Set、ZSet)和扩展类型(Bitmap、HyperLogLog、Geo、Stream)。

详细解析:

类型 底层结构 应用场景
String SDS(简单动态字符串) 缓存对象(JSON)、计数器(INCR)、分布式锁(SETNX)
List QuickList(ZipList + LinkedList) 消息队列(LPUSH/BRPOP)、最新动态、文章列表
Hash ZipList / HashTable 对象存储(用户信息)、购物车(商品ID→数量)
Set IntSet / HashTable 去重(UV)、共同好友、标签、抽奖
ZSet ZipList / SkipList + HashTable 排行榜、延迟队列(score=时间戳)、热搜排名
Bitmap String 签到打卡、布隆过滤器、活跃用户统计
HyperLogLog 稀疏/密集矩阵 UV 统计(误差 0.81%,12KB 统计 2^64)
Geo ZSet 附近的人、地图范围查询(GEOADD/GEORADIUS)
Stream RadixTree 消息队列(支持消费组、ACK)、事件流
  • ZSet 底层:跳跃表(SkipList)+ 哈希表。跳跃表支持范围查询 O(logN),哈希表支持单元素查找 O(1)。
  • String 最大 512MB,List/Set/ZSet 最大 2^32 个元素。

生产场景: 实时排行榜——百万用户按积分排名。解决方法: ZADD ranking:userId score userIdZREVRANGE ranking:userId 0 9 WITHSCORES 获取 Top 10,ZREVRANK ranking:userId userId 获取用户排名。O(logN) 复杂度,百万级数据毫秒响应。

Q51:Redis 持久化机制(RDB vs AOF)?

核心原理: RDB 是内存快照(全量),AOF 是命令日志(增量)。RDB 恢复快但可能丢数据,AOF 数据安全但恢复慢。

详细解析:

  • RDB(Redis Database)
    • SAVE(阻塞)或 BGSAVE(fork 子进程,COW 机制写快照)。
    • 触发方式:手动 BGSAVE、配置 save 900 1(900秒内1次修改)、shutdown。
    • 优点:二进制紧凑文件小,恢复快,适合备份。
    • 缺点:两次快照间数据可能丢失;fork 大内存时耗时。
  • AOF(Append Only File)
    • 记录每条写命令到 appendonly.aof 文件。
    • 刷盘策略:always(每条命令 fsync,最安全但最慢)、everysec(每秒 fsync,默认,最多丢 1 秒)、no(由 OS 决定)。
    • AOF 重写:fork 子进程,根据当前内存数据重新生成最小命令集(如 100 次 INCR 合并为 1 次 SET),减小文件体积。auto-aof-rewrite-percentage 100(文件翻倍时重写)。
    • 优点:数据安全性高(最多丢 1 秒)。
    • 缺点:文件比 RDB 大,恢复慢。
  • Redis 4.0+ 混合持久化aof-use-rdb-preamble yes,AOF 重写时前半部分写 RDB 格式(全量快照),后半部分写 AOF 增量命令。兼具 RDB 的快速恢复和 AOF 的数据安全。

生产场景: 缓存服务重启后需要快速恢复且不能丢太多数据。解决方法: 开启混合持久化 aof-use-rdb-preamble yes + appendfsync everysec,恢复时先加载 RDB 快照(秒级),再重放少量 AOF 命令。

Q52:Redis 过期策略和内存淘汰策略?

核心原理: 过期策略决定何时删除过期 key,淘汰策略决定内存满时删除哪些 key。

详细解析:

  • 过期策略(删除过期 key)
    • 惰性删除:访问 key 时检查是否过期,过期则删除。优点 CPU 友好,缺点过期 key 不访问会一直占用内存。
    • 定期删除:每 100ms 随机抽取一批设了 TTL 的 key 检查,删除过期的。如果过期比例 >25% 则继续抽取。
    • Redis 同时使用惰性 + 定期删除,互补。
  • 内存淘汰策略(内存满时)maxmemory-policy 配置:
    • noeviction:默认,内存满直接报错(写入操作失败)。
    • allkeys-lru:所有 key 中淘汰最近最少使用(最常用)。
    • allkeys-lfu:所有 key 中淘汰最不经常使用(4.0+)。
    • allkeys-random:随机淘汰。
    • volatile-lru:只在设了过期时间的 key 中 LRU 淘汰。
    • volatile-lfu:只在设了过期时间的 key 中 LFU 淘汰。
    • volatile-random:只在设了过期时间的 key 中随机淘汰。
    • volatile-ttl:淘汰即将过期的 key。
  • LRU vs LFU:LRU 最近最少使用(基于时间),LFU 最不经常使用(基于频率)。LFU 解决 LRU 的"偶尔被访问的非热点数据不会被淘汰"问题。Redis 的 LRU/LFU 是近似实现(采样淘汰,默认采样 5 个 key,maxmemory-samples)。

生产场景: Redis 作为缓存,内存满后写入失败。解决方法: 设置 maxmemory 4gb + maxmemory-policy allkeys-lru,优先淘汰最久未访问的数据。如果热点数据稳定用 LFU。纯缓存场景用 allkeys-lru,混合持久化场景用 volatile-lru(不淘汰持久化数据)。

Q53:缓存穿透、缓存击穿、缓存雪崩的区别和解决方案?

核心原理: 三者都是缓存失效导致请求打到数据库的问题,触发条件不同。

详细解析:

问题 触发条件 危害 解决方案
缓存穿透 查询不存在的数据,缓存和 DB 都没有 恶意攻击,DB 被打满 布隆过滤器拦截;缓存空值+短TTL
缓存击穿 单个热点 key 过期瞬间 大量并发请求打到 DB 互斥锁重建;逻辑过期不设TTL
缓存雪崩 大量 key 同时过期或 Redis 宕机 DB 瞬间压力暴增 TTL加随机抖动;Redis高可用;本地缓存兜底
  • 缓存穿透 - 布隆过滤器:在缓存前加布隆过滤器,存储存在的 key。请求先过布隆过滤器判断 key 是否可能存在,不存在直接返回。极小误判率(可调参数降低)。也可缓存空值(SET key null EX 300)设置短过期时间。
  • 缓存击穿 - 互斥锁:缓存未命中时,用 Redis SETNX 获取互斥锁,只有一个线程查 DB 重建缓存,其他线程等待或返回旧值。也可用"逻辑过期"(key 不设 TTL,value 中存逻辑过期时间,后台异步重建)。
  • 缓存雪崩 - 随机 TTLexpire(key, base + random(0, 300)) 避免同时过期。Redis Cluster 高可用 + Caffeine 本地缓存兜底。熔断降级(Sentinel)保护 DB。

生产场景: 黑客用不存在的 ID 大量请求接口,DB 被打满。解决方法: 布隆过滤器 + 缓存空值双重防护;网关层限流(Sentinel 热点参数限流);IP 黑名单。

Q54:Redis 和 MySQL 双写一致性如何保证?

核心原理: 缓存和数据库分属两个系统,无法保证强一致性,通常追求最终一致性。

常见方案:

  1. Cache Aside(旁路缓存,推荐)
    • 读:先查缓存,未命中查 DB 后写入缓存。
    • 写:先更新 DB,再删除缓存(删除而非更新,避免并发写导致缓存与 DB 不一致)。
    • 问题:先更新 DB 再删缓存,如果删缓存失败则数据不一致 → 延迟双删(删缓存 → 更新 DB → 延迟 500ms 再删缓存)。
  2. Canal 监听 binlog
    • Canal 模拟 MySQL 从节点,监听 binlog 变更,消费 binlog 事件后删除/更新 Redis 缓存。
    • 优点:业务代码无需关心缓存,完全解耦。
    • 缺点:有延迟(毫秒级),需保证 Canal 高可用。
  3. 消息队列保证
    • 更新 DB 后发 MQ 消息,消费者删除缓存。消息可靠投递保证最终一致。
  4. 先删缓存再更新 DB
    • 问题:删缓存后、更新 DB 前,另一个请求读到旧数据并写入缓存 → 数据不一致。
    • 解决:延迟双删(先删缓存 → 更新 DB → 延迟再删缓存)。

生产场景: 商品价格更新后缓存未及时刷新,用户看到旧价格。解决方法: 采用 Canal 监听 binlog 异步删除缓存 + 延迟双删兜底;读请求走主库(写后读强制走主库);对一致性要求极高的场景(如金融)加分布式锁保护读写。

Q55:Redis 分布式锁如何实现?

核心原理: Redis 分布式锁通过 SET key value EX seconds NX 原子命令实现,生产环境推荐 Redisson 框架。

详细解析:

  • 基本实现SET lock_key uuid EX 30 NX,NX 保证互斥,EX 设置过期避免死锁,value 用 UUID 标识持有者。
  • 解锁(Lua 脚本保证原子性)
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    
    先判断 value 是否匹配(防止误删别人的锁),再删除。
  • Redisson 增强
    • 看门狗(Watchdog):默认锁过期 30 秒,每 10 秒自动续期,业务执行时间长也不会锁过期。
    • 可重入:Hash 结构存储线程 ID + 重入次数。
    • 公平锁/读写锁/信号量:支持多种锁类型。
    • RedLock:向多个独立 Redis 节点加锁,多数成功才算获取锁,解决主从切换丢锁问题(争议较大,生产慎用)。
  • 常见陷阱
    • SETNX + EXPIRE 非原子 → 用 SET EX NX。
    • 锁过期但业务未完成 → 看门狗续期。
    • 主从切换丢锁 → RedLock 或 Redlock+ fencing token。
    • 误删别人的锁 → UUID + Lua 脚本判断。

生产场景: 定时任务多实例部署,需保证只有一个实例执行。解决方法: Redisson RLock lock = redissonClient.getLock("task:sync:lock"); lock.tryLock(0, -1, TimeUnit.SECONDS),获取锁的实例执行,其他实例跳过。或用 XXL-JOB 调度框架内置分布式锁。

Q56:Redis 集群模式(主从、哨兵、Cluster)?

核心原理: Redis 有三种集群方案:主从复制(读写分离)、哨兵模式(自动故障转移)、Cluster(分片+高可用)。

详细解析:

  • 主从复制
    • 一主多从,主写从读,数据异步复制。
    • 优点:读写分离分担读压力、数据备份。
    • 缺点:主节点故障需手动切换,无法自动恢复。
  • 哨兵模式(Sentinel)
    • 哨兵独立进程监控主从节点,主节点故障时自动选举从节点为新主。
    • 工作原理:哨兵定期 PING 主从节点,主观下线(单个哨兵认为不可用)→ 客观下线(多数哨兵确认)→ 选举 leader 哨兵执行故障转移 → 选优先级最高的从节点升主 → 通知客户端新主地址。
    • 优点:自动故障转移,高可用。
    • 缺点:单主写入瓶颈,无法水平扩展存储。
  • Redis Cluster
    • 数据分片:16384 个哈希槽(slot),CRC16(key) % 16384 定位 slot → slot 分配到不同节点。
    • 每个主节点负责一部分 slot,配 1-2 个从节点。
    • Gossip 协议:节点间通信,感知集群状态,故障检测和转移。
    • 客户端路由:MOVED 重定向(key 不在当前节点返回 MOVED 指令)、ASK 临时重定向(slot 迁移中)。
    • 优点:水平扩展(数据分散到多节点)、高可用(每个主节点有从节点)。
    • 缺点:不支持跨 slot 的多键操作(除非 hashtag {user}:1 保证同一 slot);事务和 Lua 脚本受限。

生产场景: 数据量 200GB,单机内存不够,需水平扩展。解决方法: Redis Cluster,6 个节点(3 主 3 从),每主负责约 5461 个 slot,数据均匀分布。注意跨 slot 操作需用 hashtag 保证同一 key 落在同一 slot。

Q57:Redis 大 Key 和热 Key 问题如何解决?

核心原理: 大 Key 指 value 过大(如 List 存百万元素),热 Key 指单个 key 访问量过高(如热点商品),都会影响 Redis 性能。

详细解析:

  • 大 Key 问题
    • 危害:网络阻塞(传输大 value 耗时)、主线程阻塞(删除大 key 阻塞 Redis 单线程,Redis 4.0+ 用 UNLINK 异步删除)、内存不均(Cluster 中某个 slot 过大导致节点内存倾斜)、阻塞其他操作。
    • 检测:redis-cli --bigkeys 扫描分析;MEMORY USAGE key 查看单个 key 内存。
    • 解决:拆分大 key(百万元素 List 拆成多个小 List);大 value 压缩(Snappy/Gzip)或存 OSS 只存引用;UNLINK 异步删除;设置合理 TTL。
  • 热 Key 问题
    • 危害:单 key QPS 过高导致单节点 CPU 打满(Cluster 中热 key 集中在一个节点)、缓存击穿。
    • 检测:redis-cli --hotkeys(需开启 LFU);MONITOR 命令(慎用,影响性能);代理层统计 key 访问频率。
    • 解决:多副本分散(热 key 复制多份 key_1, key_2, ..., key_N,随机读副本);本地缓存兜底(Caffeine 缓存热 key,减少 Redis 访问);key 分桶stock_{productId}_0 ~ stock_{productId}_9 随机选桶读写)。

生产场景: 秒杀商品详情页 QPS 10 万+,单个商品 key 访问集中在一个 Redis 节点。解决方法: Caffeine 本地缓存 + Redis 二级缓存,本地缓存 TTL 5 秒兜底;热 key 分桶 product_123_0 ~ product_123_9,读时随机选桶分散压力。

Q58:Redis 的管道(Pipeline)和事务(MULTI/EXEC)?

核心原理: Pipeline 是客户端批量发送命令减少网络往返,MULTI/EXEC 是服务端事务保证命令顺序执行。

详细解析:

  • Pipeline
    • 客户端将多条命令打包一次性发送,服务端依次执行后一次性返回结果。
    • 减少网络 RTT(Round Trip Time),如 100 条命令从 100 次 RTT 降为 1 次。
    • 非原子性:命令间可能穿插其他客户端命令。
    • 适合批量写入/读取场景。
  • MULTI/EXEC 事务
    • MULTI 开启事务 → 多条命令入队(返回 QUEUED)→ EXEC 原子执行所有命令。
    • 不支持回滚:某条命令运行时错误(如对字符串 INCR)不会影响其他命令执行,也不会回滚。
    • WATCH key 乐观锁:EXEC 前如果 key 被修改则事务中断(返回 nil)。
    • 原子性仅保证"顺序执行不被打断",不是数据库事务的 ACID 原子性。
  • Lua 脚本
    • EVAL script numkeys key... arg... 执行 Lua 脚本,Redis 单线程执行脚本期间不会被其他命令打断。
    • 比事务更强:真正的原子性 + 条件判断 + 循环逻辑。
    • 秒杀库存扣减的 Lua 脚本:判断库存 → 扣减 → 记录用户 → 返回结果,全部原子执行。

生产场景: 批量写入 1 万条缓存数据,逐条 SET 耗时 10 秒。解决方法: Pipeline 批量发送,100 条一批,耗时降至 1 秒内。注意 Pipeline 批量不宜过大(控制内存),建议 500-1000 条/批。

Q59:布隆过滤器原理和应用?

核心原理: 布隆过滤器是一种概率型数据结构,用位数组 + 多个哈希函数判断元素"一定不存在"或"可能存在",空间效率极高。

详细解析:

  • 结构:一个长度为 m 的位数组(初始全 0)+ k 个独立哈希函数。
  • 写入:对元素用 k 个哈希函数计算得到 k 个位置,将位数组对应位置设为 1。
  • 查询:对元素用 k 个哈希函数计算,如果所有位置都为 1 则"可能存在"(有误判),如果有任一位置为 0 则"一定不存在"(无漏报)。
  • 误判率:与位数组大小 m、哈希函数数量 k、元素数量 n 有关。m 越大误判越低,k 太多冲突反而增加。典型配置:100 万元素、1% 误判率,仅需 1.2MB 内存。
  • 不支持删除:多个元素可能哈希到同一位置,删除会影响其他元素。Counting Bloom Filter(用计数器代替位)支持删除。
  • Redis 中使用:RedisBloom 模块(BF.ADD/BF.EXISTS);或用 Redis Bitmap 手动实现;Guava BloomFilter 本地实现。

生产场景: 缓存穿透防护——查询不存在的用户 ID 打到数据库。解决方法: 系统启动时将所有用户 ID 加入布隆过滤器,查询请求先过布隆过滤器判断,不存在直接返回,不查缓存和 DB。百万用户 ID 仅需 1MB+ 内存,误判率 1%。

六、消息队列(Q60-Q67)

Q60:Kafka、RocketMQ、RabbitMQ 的区别和选型?

核心原理: 三种 MQ 在架构模型、性能、功能特性上有显著差异,需根据业务场景选型。

详细解析:

维度 Kafka RocketMQ RabbitMQ
开发语言 Scala/Java Java Erlang
性能 百万级 TPS 十万级 TPS 万级 TPS
延迟 毫秒级 毫秒级 微秒级
消息可靠性 高(副本+ack) 高(同步刷盘+副本) 高(确认机制+持久化)
顺序消息 分区内有序 分区内有序 队列内有序
事务消息 弱(幂等生产者) 强(半消息+回查) 无原生支持
延迟消息 不支持 18 个延迟级别 插件/TTL+死信
消息回溯 支持(offset) 支持(时间戳) 不支持
适用场景 日志采集、大数据流 电商交易、金融 企业应用、低延迟
  • Kafka:高吞吐、日志型场景首选。分区+副本机制,Pull 模式消费。不适合需要精确事务和延迟消息的场景。
  • RocketMQ:阿里开源,Java 实现,适合电商交易场景。原生支持事务消息、延迟消息、顺序消息、消息回溯。
  • RabbitMQ:AMQP 协议,路由灵活(Direct/Topic/Fanout/Headers),低延迟,适合复杂路由和企业应用。Erlang 实现运维门槛高。

生产场景: 电商系统需要订单支付后发消息通知发货、加积分,要求消息不丢且支持事务。解决方法: 选 RocketMQ,事务消息保证本地事务与消息发送的最终一致性,延迟消息实现订单超时取消。

Q61:如何保证消息不丢失?

核心原理: 消息从生产到消费经过三个环节(生产者→Broker→消费者),每个环节都可能丢失,需分别保障。

详细解析:

  • 生产者端(发送不丢失)
    • Kafka:acks=all(等待所有 ISR 副本确认)+ retries=3(重试)+ enable.idempotence=true(幂等避免重试导致重复)。
    • RocketMQ:同步发送(send() 等待 Broker 确认)+ 重试;或异步发送 + 回调检查。
    • RabbitMQ:开启 confirm 模式(publisher-confirms=true),Broker 收到后回调 ack。
  • Broker 端(存储不丢失)
    • Kafka:replication.factor≥3(3 副本)+ min.insync.replicas=2(至少 2 个副本同步成功)。
    • RocketMQ:flushDiskType=SYNC_FLUSH(同步刷盘,性能下降但数据安全)+ 主从同步复制。
    • RabbitMQ:队列和消息持久化(durable=true + deliveryMode=2)。
  • 消费者端(消费不丢失)
    • 关闭自动 ACK,改为手动 ACK:业务处理成功后再 basicAck,异常时 basicNack 重新入队。
    • Kafka:关闭自动提交 offset(enable.auto.commit=false),业务处理成功后手动提交。

生产场景: 订单支付消息丢失导致仓库未发货。解决方法: RocketMQ 同步发送 + 同步刷盘 + 手动 ACK + 消费幂等,全链路保障不丢。配合本地消息表兜底(定时扫描未确认消息重新投递)。

Q62:如何保证消息不重复消费(消费端幂等)?

核心原理: 网络抖动、生产者重试、消费者 ACK 失败重试都可能导致消息重复投递,需在消费端实现幂等性。

常见方案:

  1. 消息 ID + Redis 去重:消费前用 SETNX msgId 1 EX 86400,设置成功则处理,失败(已存在)则跳过。简单高效,适合大部分场景。
  2. 数据库唯一索引:业务表对业务唯一键(如订单号)建唯一索引,重复插入抛 DuplicateKeyException,捕获后视为已处理。绝对可靠。
  3. 乐观锁/状态机UPDATE order SET status='paid' WHERE id=? AND status='unpaid',影响行数=0 表示已处理。
  4. 去重表:单独建消息处理记录表,消费前查是否已处理。
  5. Redis Token 机制:与唯一索引类似,用 Redis 的 GETDEL msgId 原子操作。

生产场景: 支付成功消息重复消费导致用户积分加两次。解决方法: 消费端用消息 ID + Redis SETNX 去重(TTL 24 小时);数据库积分表对 (user_id, order_id) 建唯一索引兜底。双重保障确保幂等。

Q63:如何保证消息顺序消费?

核心原理: 全局有序难以实现且性能差,通常保证局部有序(同一业务 ID 的消息有序)。

详细解析:

  • Kafka 顺序消息
    • 生产者:同一业务 key 的消息发送到同一 partition(partition = hash(key) % partitionCount)。
    • 消费者:一个 partition 只能被消费组内一个消费者消费,保证分区内有序。
    • 问题:一个 partition 一个消费者,并发度受限。
  • RocketMQ 顺序消息
    • 生产者:MessageQueueSelector 将同一 shardingKey 的消息发到同一 MessageQueue。
    • 消费者:MessageListenerOrderly 顺序消费(同一队列加锁,单线程消费),MessageListenerConcurrently 并发消费(不保证顺序)。
    • 顺序消费失败会阻塞当前队列,重试直到成功或达到最大重试次数。
  • RabbitMQ 顺序消息:一个队列对应一个消费者,队列内 FIFO。多个消费者会破坏顺序。

生产场景: 订单状态变更消息(创建→支付→发货→完成)需按顺序消费,否则可能"已完成"先于"支付"被消费。解决方法: 以订单 ID 为 shardingKey,同一订单的消息发到同一 partition/queue,单线程消费保证顺序。注意顺序消费会降低吞吐量,仅对有顺序要求的业务使用。

Q64:消息积压如何处理?

核心原理: 消息积压是消费速度跟不上生产速度,需提升消费能力或减少生产量。

处理方案:

  1. 扩容消费者:增加消费者实例数(不能超过 partition/queue 数量,否则多余消费者空闲)。Kafka 中一个 partition 只能被一个消费者消费。
  2. 临时扩 partition + 消费者:Kafka 可以动态增加 partition 数量,同时增加消费者。
  3. 临时 Topic 批量迁移:积压严重时,新建一个 partition 数更多的临时 topic,消费者从原 topic 快速读取(不做业务处理)转发到临时 topic,再用大量消费者消费临时 topic。
  4. 提升单消费者处理速度:批量处理(攒一批一起操作 DB)、异步处理(接收后直接入库待处理表,异步慢慢消费)、多线程消费(注意顺序性)。
  5. 限流降级:上游限流减少消息生产量;非核心业务暂停消费。
  6. 排查消费慢的原因:DB 慢查询、外部接口超时、GC 频繁、消费者机器资源不足。

生产场景: 大促期间订单消息积压 500 万条,消费者跟不上。解决方法: 紧急扩容 partition 从 8 到 32,消费者从 8 到 32;消费逻辑改为批量写入 DB(每 100 条一批);非核心消息(如通知)暂停消费。积压从 500 万降到 0 耗时 30 分钟。

Q65:延迟消息如何实现?

核心原理: 延迟消息是消息发送后延迟指定时间才被消费,常用于订单超时关闭、延迟通知等场景。

实现方案:

  1. RocketMQ 延迟消息:原生支持 18 个延迟级别(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)。RocketMQ 5.0+ 支持任意时间延迟。原理:消息先存入延迟队列(按级别分 Topic),后台定时任务扫描到期消息转发到目标 Topic。
  2. RabbitMQ TTL + 死信队列:消息设置 TTL 过期后进入死信队列(DLX),消费者监听死信队列实现延迟。注意:队列 TTL 是队头消息过期才检查,可能导致后面的消息延迟不准确。
  3. Redis ZSet:score=执行时间戳,定时任务轮询 ZRANGEBYSCORE key 0 now 获取到期任务。Redisson DelayedQueue 封装更优雅。
  4. 时间轮:Kafka 内部用 Hierarchical Timing Wheel(层级时间轮),Netty 的 HashedWheelTimer 也可实现。适合大量延迟任务。
  5. 定时任务扫库:数据库存储待执行任务 + 执行时间,定时任务扫描到期任务执行。精度低但简单可靠。

生产场景: 订单 15 分钟未支付自动取消。解决方法: 下单时发送 RocketMQ 延迟 15 分钟消息,消费时检查订单状态:已支付则忽略,未支付则取消订单 + 释放库存。消费端需幂等(订单可能已被手动取消)。

Q66:RocketMQ 事务消息的原理?

核心原理: RocketMQ 事务消息通过半消息 + 本地事务 + 回查机制,保证本地事务与消息发送的最终一致性。

详细流程:

  1. 发送半消息:Producer 向 Broker 发送半消息(Half Message),此消息对 Consumer 不可见(存入特殊 Topic RMQ_SYS_TRANS_HALF_TOPIC)。
  2. 执行本地事务:Producer 执行本地数据库事务(如创建订单)。
  3. 提交/回滚
    • 本地事务成功 → Producer 向 Broker 发送 COMMIT,半消息转为可消费消息(转发到目标 Topic)。
    • 本地事务失败 → Producer 发送 ROLLBACK,半消息被删除。
  4. 事务回查:如果 Producer 发送 COMMIT/ROLLBACK 失败(网络问题或 Producer 宕机),Broker 定期(60 秒)回查 Producer 的本地事务状态:checkLocalTransaction() 返回 COMMIT/ROLLBACK/UNKNOWN。
  5. 超时处理:回查超过一定次数(默认 15 次)后,Broker 自动 ROLLBACK。

生产场景: 订单支付成功后需通知发货和积分系统,要求本地支付记录与消息发送一致。解决方法: 用 RocketMQ 事务消息——发送半消息 → 更新支付状态为"已支付" → COMMIT。如果本地事务成功但 COMMIT 丢失,Broker 回查 checkLocalTransaction() 返回"已支付" → COMMIT。消费者(发货/积分)收到消息后处理,消费端幂等保证不重复。

Q67:消息队列如何实现削峰填谷?

核心原理: MQ 作为缓冲层,突发流量先写入 MQ(削峰),消费者按自身能力匀速消费(填谷),保护下游系统。

详细解析:

  • 削峰:前端请求高峰时,同步写入 MQ 后立即返回"排队中"(而非等待业务处理完成),MQ 承受流量峰值。
  • 填谷:消费者以固定速率(或根据自身处理能力)从 MQ 拉取消息处理,将突发流量平滑为匀速流量。
  • 异步解耦:上游不需要等待下游处理完成,响应时间从"业务处理耗时"降为"写入 MQ 耗时"(毫秒级)。
  • 配合限流:消费者端用 Sentinel/RateLimiter 限制消费速率,避免消费过快打垮下游 DB。

生产场景: 秒杀活动 QPS 峰值 10 万,但订单处理系统最大承受 2000 QPS。解决方法: 请求先写入 Kafka(10 万 QPS 峰值),订单消费者限速 2000 QPS 消费,超出能力积压在 Kafka 中。用户收到"排队中"提示,前端轮询查询结果。活动结束后消费者继续消费积压消息,10 分钟内处理完毕。

七、分布式系统(Q68-Q77)

Q68:CAP 理论和 BASE 理论?

核心原理: CAP 指分布式系统无法同时满足一致性(C)、可用性(A)、分区容错性(P),只能选其二。BASE 是 CAP 的实践妥协,追求最终一致性。

详细解析:

  • CAP 理论
    • 一致性(Consistency):所有节点同一时刻看到相同数据。
    • 可用性(Availability):每个请求都能收到响应(不保证是最新数据)。
    • 分区容错性(Partition Tolerance):网络分区时系统仍能运行。
    • 由于网络分区不可避免,实际只能在 CP 和 AP 间选择:
      • CP:ZooKeeper、etcd、Redis(主从切换时拒绝写入)。保证一致但可能不可用。
      • AP:Eureka、Cassandra、Redis Cluster(故障时仍可读写)。保证可用但可能数据不一致。
  • BASE 理论
    • Basically Available(基本可用):允许损失部分可用性(响应时间增加、降级服务)。
    • Soft State(软状态):允许中间状态存在(数据同步有延迟)。
    • Eventually Consistent(最终一致性):系统保证最终数据一致,不要求实时强一致。
  • 大部分互联网系统选择 AP + 最终一致性(BASE),金融核心场景选择 CP(强一致)。

生产场景: 电商下单场景中库存扣减和订单创建跨服务,需保证一致性但不能阻塞太久。解决方法: 放弃强一致性,用 RocketMQ 事务消息 + 本地消息表保证最终一致性(BASE)。支付场景则用 TCC 强一致保证资金安全。

Q69:分布式事务有哪些解决方案?

核心原理: 分布式事务解决跨库/跨服务的数据一致性问题,常见方案有 2PC、TCC、SAGA、本地消息表、MQ 事务消息。

详细解析:

方案 一致性 性能 复杂度 适用场景
2PC/XA 强一致 差(锁等待) 传统数据库,少用
TCC 强一致 好(无锁) 高(需写3个接口) 金融核心交易
SAGA 最终一致 中(需补偿) 长流程编排
本地消息表 最终一致 跨服务最终一致
MQ 事务消息 最终一致 RocketMQ 生态
  • 2PC(两阶段提交):协调者发起 prepare → 参与者锁定资源返回 yes/no → 协调者根据全部响应发 commit/rollback。问题:协调者单点故障、同步阻塞、数据不一致(commit 阶段部分参与者超时)。
  • TCC(Try-Confirm-Cancel):Try 预留资源(冻结库存)→ Confirm 确认(实际扣减)→ Cancel 回滚(释放冻结)。需处理空回滚(Cancel 先于 Try)、悬挂(Try 在 Cancel 后到达)、幂等。性能好但业务侵入强。
  • SAGA:长事务拆分为多个子事务,每个有正向操作和补偿操作。任一失败则反向执行已完成子事务的补偿。适合跨多服务长流程。
  • 本地消息表:业务表中附带消息表,本地事务保证业务+消息原子写入,定时任务扫描消息表投递 MQ。
  • Seata:提供 AT(自动补偿,基于 undo_log)、TCC、SAGA、XA 四种模式。AT 模式侵入最低。

生产场景: 下单需扣库存(库存服务)+ 创建订单(订单服务)+ 冻结优惠券(券服务)。解决方法: 高并发秒杀用 TCC(性能优先);普通下单用 Seata AT(低侵入)或 RocketMQ 事务消息(最终一致)。

Q70:分布式 ID 生成方案?

核心原理: 分库分表后数据库自增 ID 无法保证全局唯一,需独立 ID 生成方案。

常见方案:

  1. UUID:本地生成无网络开销,全局唯一。缺点:无序(B+ 树插入碎片多)、过长(36 字符)、无业务含义。适合非数据库主键场景。
  2. Snowflake(雪花算法):64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。趋势递增、高性能(本地生成)。缺点:时钟回拨问题(机器时间回退导致 ID 重复)。
  3. Leaf-Segment(美团):数据库号段模式,每次申请一个 ID 段(如 1000 个)缓存在本地,用完再申请。双 buffer 优化:当前 buffer 用到 10% 时异步加载下一个。适合高并发,依赖 DB 但压力小。
  4. Leaf-Snowflake(美团):ZooKeeper 分配机器 ID,解决时钟回拨(记录上次时间戳,回拨时等待或报错)。
  5. Redis INCRINCR id_generator 简单原子,但依赖 Redis 可用性,Redis 故障有风险。
  6. 数据库多主自增:多个 MySQL 实例设置不同初始值和步长(如实例1从1步长2,实例2从2步长2),扩展性差。

生产场景: 日增 100 万订单,分 16 张表,需全局唯一订单 ID。解决方法: Snowflake 生成 64 位 long 型 ID,趋势递增利于 B+ 树索引。机器 ID 通过 ZooKeeper/配置中心分配。订单 ID 可嵌入业务含义(如 时间戳 + 用户ID后4位 + 序列号)。

Q71:一致性哈希原理?

核心原理: 一致性哈希将整个哈希值空间组织成虚拟圆环(0 ~ 2³²-1),节点和 key 都映射到环上,key 顺时针找到最近的节点。解决普通取模哈希在节点增减时大量数据迁移的问题。

详细解析:

  • 普通取模的问题hash(key) % N,N 变化时几乎所有 key 需要重新映射。如 N=4 增加到 N=5,75% 的数据需迁移。
  • 一致性哈希
    • 节点映射:对节点 IP/名称取 hash 映射到环上。
    • key 映射:对 key 取 hash 映射到环上,顺时针找到第一个节点。
    • 节点增减:只影响相邻区间的 key,迁移量 = 1/N。
  • 数据倾斜问题:节点少时可能集中在环的某一段,导致负载不均。
  • 虚拟节点解决倾斜:每个物理节点对应 150 个虚拟节点(node#1, node#2, ..., node#150),均匀分布在环上。虚拟节点越多分布越均匀,但内存开销增加。
  • 应用场景:Redis Cluster(实际用 16384 slot 而非一致性哈希)、Memcached 客户端、负载均衡。

生产场景: 缓存集群从 4 节点扩容到 6 节点。解决方法: 一致性哈希 + 虚拟节点,只有约 1/6 的数据需要迁移(而非 2/3)。对比普通取模的 50%+ 迁移量大幅减少。

Q72:限流算法有哪些?

核心原理: 限流算法控制单位时间内的请求量,保护系统不被打垮。常见有固定窗口、滑动窗口、令牌桶、漏桶四种。

详细解析:

  • 固定窗口计数:将时间划分为固定窗口(如每秒),窗口内计数超限则拒绝。缺点:窗口边界突刺(0.9s 和 1.1s 各放 100 请求,1 秒内实际 200 请求)。
  • 滑动窗口计数:将窗口细分为小格子(如 1 秒分为 10 个 100ms 格子),滑动统计。比固定窗口平滑,但仍有边界问题。Sentinel 默认用滑动窗口。
  • 漏桶算法(Leaky Bucket):请求像水滴入桶,桶以固定速率漏水。桶满则拒绝。匀速输出,保护下游,但不允许突发流量。
  • 令牌桶算法(Token Bucket):以固定速率往桶中放令牌,请求获取令牌才处理,桶满则丢弃多余令牌。允许突发流量(桶中令牌可积攒),Guava RateLimiter 实现。
  • 对比:令牌桶允许突发(适合前端限流),漏桶匀速输出(适合保护下游)。
  • 分布式限流:Redis + Lua 脚本实现原子计数(ZSet 滑动窗口:score=时间戳,ZREMRANGEBYSCORE 清理过期 + ZCARD 计数)。Sentinel 集群限流。

生产场景: API 网关限制每个用户每秒最多 100 次请求。解决方法: 令牌桶(Guava RateLimiter 单机 / Redis + Lua 分布式),允许短时突发但平均速率可控。Sentinel 热点参数限流可按用户 ID 维度限流。

Q73:分布式系统的设计模式有哪些?

核心原理: 分布式系统设计模式是解决分布式架构中常见问题的最佳实践。

常见模式:

  1. Saga 模式:长事务拆分为子事务序列,每个有补偿操作,失败时反向补偿。
  2. Outbox Pattern(发件箱模式):本地事务中同时写业务数据和 outbox 表,后台读取 outbox 发送到 MQ。解决"数据库提交和消息发送"的原子性问题。
  3. CQRS(命令查询职责分离):写操作和读操作分离到不同模型/存储。写入 MySQL,读取走 ES/Redis。适合读写比例差异大的场景。
  4. Event Sourcing(事件溯源):不存储当前状态,而是存储所有状态变更事件。通过重放事件重建状态。适合审计追溯场景。
  5. Circuit Breaker(熔断器):下游故障时快速失败,避免级联雪崩。Hystrix/Sentinel 实现。
  6. Bulkhead(舱壁隔离):不同业务用独立线程池/连接池,隔离故障。类似船舱隔水设计。
  7. Sidecar(边车模式):辅助服务与主服务部署在一起(如 Istio 的 Envoy 代理),处理网络、监控、安全等横切关注点。
  8. Service Mesh(服务网格):Sidecar 的规模化应用,将服务间通信从业务代码中剥离到基础设施层。

生产场景: 订单系统需同时写 MySQL(事务)和发 MQ 通知。解决方法: Outbox Pattern——订单事务中同时写入 orders 表和 outbox 表(同一本地事务保证原子性),Canal/定时任务读取 outbox 表投递 MQ,投递成功后标记 outbox 已发送。

Q74:服务注册与发现原理?

核心原理: 服务注册与发现是微服务架构的基础设施,服务启动时注册到注册中心,调用方从注册中心获取服务列表并负载均衡调用。

详细解析:

  • 注册中心核心功能
    • 服务注册:服务启动时将自己的地址(IP:Port)注册到注册中心。
    • 服务发现:调用方从注册中心获取目标服务的实例列表。
    • 健康检查:注册中心定期检查服务实例健康状态,剔除不健康实例。
    • 通知机制:服务列表变化时推送给订阅者(长轮询/推送)。
  • 常见注册中心对比
    • Eureka(AP):去中心化 P2P 架构,节点间复制数据。优先可用性,网络分区时仍可注册发现。已停止维护。
    • Nacos(AP/CP 可切换):阿里开源,支持 AP 和 CP 模式切换。同时提供服务注册和配置中心功能。
    • Consul(CP):基于 Raft 协议强一致,支持健康检查、KV 存储、多数据中心。
    • ZooKeeper(CP):基于 ZAB 协议强一致,临时节点+Watch 机制。偏重一致性,适合强一致场景。
    • etcd(CP):基于 Raft 协议,Kubernetes 内部使用。
  • 健康检查方式:客户端心跳(Eureka 客户端定期发心跳)、TCP/HTTP 探针(Consul/Nacos 主动探测)。
  • 负载均衡:客户端负载均衡(Ribbon/LoadBalancer,从注册中心拉取列表后本地选择),服务端负载均衡(Nginx/API 网关)。

生产场景: 微服务 50 个实例,某个实例宕机需快速剔除。解决方法: Nacos 健康检查 5 秒一次,实例 15 秒无心跳标记为不健康,30 秒后剔除。调用方通过 Nacos 客户端订阅服务变更,实例列表秒级更新。配合熔断器(Sentinel)防止调用不健康实例。

Q75:分布式缓存一致性方案?

核心原理: 分布式环境下多级缓存(本地缓存 + Redis + DB)的一致性是难点,通常通过失效模式 + 事件通知实现最终一致。

常见方案:

  1. Cache Aside + 延迟双删:先删缓存 → 更新 DB → 延迟 500ms 再删缓存。解决"先删缓存后更新 DB 期间其他线程读到旧数据写回缓存"的问题。
  2. Canal 监听 binlog:Canal 模拟 MySQL 从节点,监听 binlog 变更,消费后删除/更新各级缓存。业务代码无需感知缓存。
  3. 消息广播:更新 DB 后发 MQ 广播消息,各节点收到后清除本地缓存。Redis 缓存统一删除。
  4. Redis Pub/Sub:一个节点更新缓存后通过 Redis 发布消息,其他节点订阅后清除本地缓存。
  5. 版本号/时间戳:缓存中带版本号,读时比对版本号,旧版本则重新加载。

生产场景: 多级缓存(Caffeine + Redis),一个节点更新数据后其他节点的本地缓存未刷新。解决方法: Redis Pub/Sub 广播缓存失效消息,各节点收到后清除对应 Caffeine 缓存。配合 Canal 监听 binlog 兜底(防止 Pub/Sub 消息丢失)。

Q76:Seata 分布式事务框架的原理?

核心原理: Seata 是阿里开源的分布式事务解决方案,核心是全局事务(TC 协调器)+ 分支事务(RM 资源管理器)+ 事务发起者(TM 事务管理器)。

详细解析:

  • 三个角色
    • TC(Transaction Coordinator):独立部署的事务协调器,维护全局事务和分支事务状态,决定提交或回滚。
    • TM(Transaction Manager):定义全局事务的边界(begin/commit/rollback),通知 TC。
    • RM(Resource Manager):管理分支事务上的资源(数据库),向 TC 注册分支事务,上报状态。
  • AT 模式(推荐,低侵入)
    • 一阶段:拦截 SQL,执行业务 SQL 前记录 before image,执行后记录 after image,存入 undo_log 表。本地事务提交(不阻塞)。向 TC 上报分支状态。
    • 二阶段-提交:TC 通知 RM 提交 → RM 异步删除 undo_log(无需做其他操作,因为一阶段已提交)。
    • 二阶段-回滚:TC 通知 RM 回滚 → RM 根据 undo_log 的 before image 反向生成补偿 SQL 还原数据。
    • 全局锁:AT 模式在写操作时获取全局锁(存入 lock_table),保证写隔离。读隔离需用 @GlobalLockSELECT FOR UPDATE
  • TCC 模式:需手动实现 Try/Confirm/Cancel 三个接口。Try 预留资源,Confirm 确认,Cancel 回滚。
  • SAGA 模式:长事务编排,每步有正向和补偿操作。适合流程长、涉及外部系统的场景。

生产场景: 订单服务调用库存服务和优惠券服务,需保证三服务数据一致。解决方法: Seata AT 模式,订单服务加 @GlobalTransactional,各服务引入 Seata SDK 自动管理 undo_log。一阶段各服务本地事务提交(不阻塞),任一失败 TC 协调全局回滚。注意 AT 模式有短暂不一致窗口(一阶段已提交但全局未提交)。

Q77:如何设计一个限流系统?

核心原理: 限流系统需支持多维度(IP、用户、接口、全局)、多算法(令牌桶/漏桶/滑动窗口)、多层级(网关层/应用层/分布式)。

设计要点:

  • 多级限流架构
    1. Nginx 层:limit_req 漏桶限流,接入层第一道防线。
    2. 网关层:Spring Cloud Gateway / Sentinel 全局限流。
    3. 应用层:Sentinel 注解 @SentinelResource 精确限流(接口/方法级)。
    4. 分布式层:Redis + Lua 原子脚本实现跨实例限流。
  • 限流维度
    • 全局限流:保护系统总容量(如 10000 QPS)。
    • 接口限流:保护单个接口(如支付接口 1000 QPS)。
    • 用户限流:防止单用户刷接口(如每用户 100 QPS)。
    • IP 限流:防止恶意 IP 攻击。
  • 算法选择:令牌桶(允许突发,适合前端入口)、漏桶(匀速输出,保护下游)、滑动窗口(精确统计)。
  • 熔断降级联动:限流触发后可选择快速失败、排队等待、降级返回默认值。
  • 监控告警:实时监控限流触发率,触发率 >10% 告警,可能需扩容。

生产场景: API 网关需对每个租户限流(不同租户不同配额)。解决方法: Sentinel 集群限流 + 热点参数限流,以 tenantId 为参数维度限流。租户配额存储在配置中心(Nacos),动态调整。限流后返回 429 + Retry-After 头。

八、Spring 生态(Q78-Q87)

Q78:Spring IOC 的原理?

核心原理: IOC(Inversion of Control,控制反转)是将对象的创建和依赖注入交给 Spring 容器管理,核心是 BeanFactory 和 ApplicationContext。

详细解析:

  • IOC 容器
    • BeanFactory:顶层接口,提供最基础的 Bean 管理(延迟加载,getBean 时才创建)。
    • ApplicationContext:扩展接口,提供事件发布、国际化、AOP 等功能(启动时预加载所有单例 Bean)。
    • 常用实现:ClassPathXmlApplicationContext(XML)、AnnotationConfigApplicationContext(注解)。
  • Bean 注册方式:XML <bean>@Component/@Service/@Repository@Bean 方法、@Import
  • 依赖注入方式:构造器注入(推荐,不可变、易测试)、Setter 注入、字段注入(@Autowired,不推荐)。
  • DI 实现原理
    1. 资源定位:扫描 @ComponentScan 包下的类,读取注解元数据。
    2. BeanDefinition 注册:将每个 Bean 的元数据(类名、作用域、依赖等)封装为 BeanDefinition,注册到 BeanDefinitionRegistry。
    3. Bean 实例化:根据 BeanDefinition 通过反射创建对象(调用构造方法)。
    4. 属性注入:解析 @Autowired/@Resource,通过反射设置属性值。
    5. 初始化:调用 @PostConstructInitializingBean.afterPropertiesSet()init-method
    6. Bean 就绪:放入单例池(singletonObjects)。
  • 三级缓存解决循环依赖:见 Q81。

生产场景: 需要根据配置动态选择不同实现类(如不同支付渠道)。解决方法: 定义 PaymentService 接口,各渠道实现类加 @Service("alipay") 等,注入时用 @Qualifier("alipay")@Resource(name="alipay")。或用 @Conditional 根据条件注册 Bean。

Q79:Spring AOP 的原理?

核心原理: AOP(Aspect-Oriented Programming,面向切面编程)通过动态代理在不修改源码的情况下增强方法功能。Spring AOP 基于 JDK 动态代理或 CGLIB 实现。

详细解析:

  • 核心概念
    • 切面(Aspect):封装横切关注点的类(@Aspect)。
    • 切点(Pointcut):定义哪些方法被增强(@Pointcut("execution(* com.example.service.*.*(..))"))。
    • 通知(Advice):增强的逻辑,分为 Before、After、AfterReturning、AfterThrowing、Around。
    • 织入(Weaving):将切面应用到目标对象的过程(Spring 用运行时动态代理)。
  • 代理选择
    • 目标类实现接口 → JDK 动态代理(Proxy + InvocationHandler)。
    • 目标类无接口 → CGLIB 代理(生成子类)。
    • spring.aop.proxy-target-class=true → 强制 CGLIB。
  • 通知执行顺序:Around-before → Before → 目标方法 → Around-after → AfterReturning → After → AfterThrowing(异常时)。
  • AspectJ vs Spring AOP:Spring AOP 基于运行时动态代理(仅支持方法级增强);AspectJ 基于编译时/加载时字节码织入(支持字段、构造器级增强,性能更好但需编译器支持)。
  • 应用场景:日志记录、性能监控、事务管理(@Transactional)、权限校验、缓存(@Cacheable)。

生产场景: 统一记录所有 Service 方法的调用日志和耗时。解决方法: 定义 @Aspect 切面 + @Around("execution(* com.example.service..*.*(..))") 环绕通知,记录方法名、入参、耗时、异常。注意 AOP 失效问题:同类内部方法调用不经过代理(可通过 AopContext.currentProxy() 获取代理对象解决)。

Q80:Spring Bean 的生命周期?

核心原理: Bean 生命周期分为:实例化 → 属性注入 → 初始化 → 使用 → 销毁五个阶段,每个阶段有扩展点。

详细流程:

  1. 实例化:通过反射调用构造方法创建对象(createBeanInstance)。
  2. 属性注入populateBean,解析 @Autowired/@Resource 注入依赖。
  3. Aware 接口回调:如果 Bean 实现了 BeanNameAware(获取 Bean 名)、BeanFactoryAware(获取工厂)、ApplicationContextAware(获取上下文),执行回调。
  4. BeanPostProcessor 前置处理postProcessBeforeInitialization,所有 BeanPostProcessor 对 Bean 进行前置增强(如 @PostConstruct 由 CommonAnnotationBeanPostProcessor 处理)。
  5. 初始化方法:依次调用 @PostConstructInitializingBean.afterPropertiesSet()init-method
  6. BeanPostProcessor 后置处理postProcessAfterInitialization,如 AOP 代理在此阶段生成(AbstractAutoProxyCreator)。
  7. Bean 就绪:放入单例池,可被使用。
  8. 销毁:容器关闭时依次调用 @PreDestroyDisposableBean.destroy()destroy-method

生产场景: 需要在 Bean 初始化完成后执行数据预热(如加载配置到内存)。解决方法: @PostConstruct 注解方法中执行预热逻辑;或实现 InitializingBean 接口;或 @Bean(initMethod = "init")。注意不要在构造方法中注入依赖(构造方法执行时依赖还未注入)。

Q81:Spring 如何解决循环依赖?

核心原理: Spring 通过三级缓存机制解决单例 Bean 的 setter/字段注入循环依赖,核心是用提前暴露半成品 Bean 的方式打破循环。

详细解析:

  • 三级缓存
    • 一级缓存(singletonObjects):存放完全初始化好的 Bean(成品)。
    • 二级缓存(earlySingletonObjects):存放提前暴露的半成品 Bean(已实例化但未完成属性注入和初始化)。
    • 三级缓存(singletonFactories):存放 Bean 的 ObjectFactory(对象工厂,调用 getObject() 生成半成品 Bean)。
  • 解决流程(A 依赖 B,B 依赖 A)
    1. 创建 A:实例化 A → 将 A 的 ObjectFactory 放入三级缓存。
    2. 注入 A 的属性 B:从一级缓存找 B(没有)→ 创建 B。
    3. 实例化 B → 将 B 的 ObjectFactory 放入三级缓存。
    4. 注入 B 的属性 A:一级缓存找 A(没有)→ 二级缓存找 A(没有)→ 三级缓存找 A 的 ObjectFactory → 调用 getObject() 获取半成品 A → 放入二级缓存,删除三级缓存。
    5. B 获得半成品 A,完成属性注入和初始化 → B 放入一级缓存。
    6. A 获得成品 B,完成属性注入和初始化 → A 放入一级缓存。
  • 为什么需要三级缓存而非二级:如果 Bean 被 AOP 代理,三级缓存的 ObjectFactory 可以在获取时判断是否需要创建代理对象,保证注入的是代理对象而非原始对象。
  • 无法解决的循环依赖:构造器注入循环依赖(实例化阶段就需要依赖,无法提前暴露半成品)。解决:改用 setter 注入或 @Lazy 延迟注入。

生产场景: AService 和 BService 互相依赖,启动报 BeanCurrentlyInCreationException。解决方法: 改为 setter 注入或字段注入(Spring 自动解决);或在其中一个加 @Lazy 延迟加载;更好的做法是重构消除循环依赖(提取公共逻辑到 CService)。

Q82:Spring 事务的原理和传播机制?

核心原理: Spring 事务基于 AOP 实现,通过 @Transactional 注解标记方法,代理对象在方法执行前后管理事务的开启、提交、回滚。

详细解析:

  • 实现原理
    • @TransactionalTransactionInterceptor(AOP 通知)拦截。
    • 方法执行前:通过 PlatformTransactionManager 开启事务(getTransaction),绑定到当前线程(ThreadLocal)。
    • 方法执行:业务逻辑在事务内执行。
    • 方法正常返回:提交事务(commit)。
    • 方法抛出异常:检查回滚规则(默认只回滚 RuntimeException 和 Error),匹配则回滚(rollback)。
  • 7 种传播机制
    • REQUIRED(默认):有事务加入,无事务新建。
    • REQUIRES_NEW:总是新建事务,挂起当前事务。适合日志记录(不受外层事务影响)。
    • NESTED:有事务则创建嵌套事务(savepoint),无则新建。外层回滚则嵌套也回滚,嵌套回滚不影响外层。
    • SUPPORTS:有事务加入,无事务非事务执行。
    • NOT_SUPPORTED:非事务执行,挂起当前事务。
    • MANDATORY:必须在事务中,否则抛异常。
    • NEVER:必须非事务,否则抛异常。
  • 隔离级别@Transactional(isolation = Isolation.READ_COMMITTED),默认用数据库隔离级别。

生产场景: 事务方法内调用另一个事务方法,内层事务异常不影响外层。解决方法: 内层方法用 @Transactional(propagation = Propagation.REQUIRES_NEW),新开独立事务。注意:同类内部调用不经过代理,传播机制不生效,需通过注入自身代理或拆分到不同类。

Q83:@Transactional 注解失效的场景有哪些?

核心原理: @Transactional 基于 AOP 动态代理,当代码不经过代理对象调用时,事务注解失效。

常见失效场景:

  1. 同类内部方法调用methodA() 直接调用 this.methodB(),不经过代理对象 → 事务失效。解决:注入自身代理 @Lazy @Autowired private XxxService self; self.methodB()AopContext.currentProxy()
  2. 方法非 public:Spring 事务默认只对 public 方法生效(@Transactional 标注在 private/protected 方法上不报错但不生效)。
  3. 异常被 catch 吞掉:方法内 try-catch 捕获异常未重新抛出,事务不知道发生异常 → 不回滚。解决:catch 后 throw new RuntimeException(e) 或手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
  4. 回滚异常类型不匹配:默认只回滚 RuntimeException,checked exception 不回滚。解决:@Transactional(rollbackFor = Exception.class)
  5. 数据库引擎不支持事务:MyISAM 引擎不支持事务。
  6. Bean 未被 Spring 管理:类未被 @Component/@Service 等注解标记,或 new 创建的对象。
  7. 传播机制配置不当@Transactional(propagation = Propagation.NOT_SUPPORTED) 显式非事务执行。

生产场景: Service 方法中调用 this.saveLog() 记录日志,saveLog 上的 @Transactional(REQUIRES_NEW) 不生效,日志和业务在同一事务。解决方法: 将 saveLog 拆到独立的 LogService 类中注入调用;或用 AopContext.currentProxy().saveLog()(需开启 @EnableAspectJAutoProxy(exposeProxy = true))。

Q84:Spring Boot 自动配置原理?

核心原理: Spring Boot 自动配置通过 @SpringBootApplication@EnableAutoConfigurationspring.factories(或 AutoConfiguration.imports)加载自动配置类,根据条件注解决定是否生效。

详细解析:

  • 启动注解链@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
  • @EnableAutoConfiguration:通过 AutoConfigurationImportSelector 加载 META-INF/spring.factories(Spring Boot 2.x)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)中配置的自动配置类。
  • 条件注解
    • @ConditionalOnClass:类路径存在指定类时生效。
    • @ConditionalOnMissingBean:容器中不存在指定 Bean 时生效(允许用户覆盖默认配置)。
    • @ConditionalOnProperty:配置项满足条件时生效(如 spring.datasource.url 存在才配置数据源)。
    • @ConditionalOnWebApplication:Web 应用时生效。
  • 自动配置示例DataSourceAutoConfiguration 检测到 classpath 有数据源驱动 + 配置了 spring.datasource.url → 自动创建 DataSource Bean。
  • 自定义 Starter:创建 autoconfigure 模块(配置类 + spring.factories)+ starter 模块(依赖管理),实现开箱即用。

生产场景: 需要根据不同环境(开发/测试/生产)自动切换数据源配置。解决方法: @ConditionalOnProperty(name = "env", havingValue = "prod") 区分环境自动配置;@Profile("prod") 配合 spring.profiles.active 激活。

Q85:Spring Boot 启动流程?

核心原理: Spring Boot 启动通过 SpringApplication.run() 完成,核心步骤:创建 SpringApplication → 准备环境 → 创建 ApplicationContext → 加载 BeanDefinition → refresh 容器 → 执行 Runner。

详细流程:

  1. 创建 SpringApplication:推断应用类型(Servlet/Reactive/None)、加载 spring.factories 中的初始化器和监听器、推断主类。
  2. run() 方法
    • 获取 SpringApplicationRunListener(启动事件监听)。
    • 准备 Environment:加载 application.yml/application.properties、命令行参数、环境变量。
    • 创建 ApplicationContext(Servlet 应用创建 AnnotationConfigServletWebServerApplicationContext)。
    • prepareContext:设置 Environment、加载主类的 BeanDefinition。
    • refreshContext:核心——执行 BeanFactoryPostProcessor(扫描注册 BeanDefinition)→ 创建 Bean(实例化、注入、初始化)→ 启动内嵌 Web 服务器(Tomcat/Netty)。
    • 发布 ApplicationReadyEvent
    • 执行 CommandLineRunnerApplicationRunner
  3. 内嵌容器启动onRefresh() 阶段创建 Tomcat,绑定端口,注册 DispatcherServlet。

生产场景: 需要在启动完成后执行初始化任务(如预热缓存、注册到配置中心)。解决方法: 实现 CommandLineRunnerApplicationRunner 接口,@Order 控制执行顺序。注意:如果初始化失败会阻止应用启动,需 try-catch 降级。

Q86:Spring MVC 的请求处理流程?

核心原理: Spring MVC 通过 DispatcherServlet 统一分发请求,核心流程:前端控制器 → HandlerMapping → HandlerAdapter → 执行 Handler → ViewResolver → 视图渲染。

详细流程:

  1. 请求到达 DispatcherServlet:客户端发送请求,DispatcherServlet(前端控制器)拦截所有请求(/)。
  2. HandlerMapping 查找 Handler:根据 URL 找到对应的 HandlerExecutionChain(Controller 方法 + 拦截器)。@RequestMappingHandlerMapping 解析 @RequestMapping 注解。
  3. HandlerAdapter 执行 HandlerRequestMappingHandlerAdapter 调用 Controller 方法,处理参数绑定(@RequestParam@RequestBody@PathVariable)、数据验证(@Valid)、返回值处理。
  4. Handler 返回结果
    • 返回 View(传统 JSP):ViewResolver 解析视图名 → 渲染页面。
    • 返回 JSON(@ResponseBody/@RestController):HttpMessageConverter(如 MappingJackson2HttpMessageConverter)将对象序列化为 JSON 写入响应。
  5. 拦截器执行preHandle(Handler 执行前)→ postHandle(Handler 执行后、视图渲染前)→ afterCompletion(视图渲染后)。
  6. 异常处理@ExceptionHandlerHandlerExceptionResolver 统一处理异常。

生产场景: 统一异常处理返回标准错误格式。解决方法: @RestControllerAdvice + @ExceptionHandler(Exception.class) 捕获全局异常,统一返回 {code, message, data} 格式。自定义 BusinessException 区分业务异常和系统异常。

Q87:Spring Cloud 核心组件有哪些?

核心原理: Spring Cloud 提供微服务治理的完整解决方案,包括服务注册发现、配置中心、负载均衡、熔断限流、网关、链路追踪等。

详细解析:

功能 Spring Cloud Netflix(一代) Spring Cloud Alibaba(二代)
服务注册发现 Eureka Nacos
配置中心 Spring Cloud Config Nacos Config
负载均衡 Ribbon LoadBalancer
熔断限流 Hystrix Sentinel
网关 Zuul Spring Cloud Gateway
服务调用 Feign OpenFeign
链路追踪 Sleuth + Zipkin SkyWalking
分布式事务 Seata
  • Nacos:阿里开源,集服务注册发现 + 配置中心于一体,支持 AP/CP 切换。
  • OpenFeign:声明式 HTTP 客户端,@FeignClient 接口定义远程调用,集成负载均衡和熔断。
  • Sentinel:流量控制、熔断降级、系统负载保护,支持热点参数限流和实时监控。
  • Spring Cloud Gateway:基于 WebFlux 的反应式网关,支持路由、过滤器、限流。
  • Seata:分布式事务解决方案(AT/TCC/SAGA/XA)。

生产场景: 微服务从 Spring Cloud Netflix 迁移到 Spring Cloud Alibaba。解决方法: Eureka → Nacos(注册中心 + 配置中心一体)、Hystrix → Sentinel(更丰富的流控规则)、Zuul → Gateway(性能更好)。逐步迁移,双注册过渡。

九、微服务与高可用(Q88-Q95)

Q88:什么是服务雪崩?如何防止?

核心原理: 服务雪崩是分布式系统中一个下游服务故障,通过调用链路逐级向上传导,导致上游服务线程池/连接池耗尽,最终引发整个集群级联故障。

详细解析:

  • 雪崩过程:服务 D 响应慢 → 服务 C 调用 D 超时,线程堆积 → 服务 C 线程池耗尽,不可用 → 服务 B 调用 C 失败,线程堆积 → 服务 B 不可用 → 全站不可用。
  • 根本原因:同步调用中调用方等待被调用方响应,被调用方慢导致调用方资源(线程、连接)被耗尽。
  • 预防措施
    1. 熔断(Circuit Breaker):下游错误率超阈值时"断路",快速失败不再调用,定期探测恢复。Sentinel/Hystrix 实现。
    2. 降级(Fallback):熔断或超时后返回默认值/缓存数据/友好提示,保证核心功能可用。
    3. 限流(Rate Limiting):控制请求速率,防止过载。令牌桶/漏桶算法。
    4. 隔离(Bulkhead):不同服务用独立线程池/信号量,故障不扩散。
    5. 超时设置:所有远程调用设置合理超时(如 Feign 超时 3 秒),避免无限等待。
    6. 重试限制:重试次数有限(1-3 次),避免重试风暴加剧下游压力。

生产场景: 库存服务 DB 慢查询导致响应 5 秒,订单服务线程池 30 秒耗尽,10 秒后全站雪崩。解决方法: Feign 超时 2 秒 + Sentinel 熔断(慢调用比例 >50% 熔断 10 秒)+ 降级返回缓存库存 + 订单/库存独立线程池隔离。雪崩不再发生,故障隔离在库存服务。

Q89:Sentinel 限流熔断的原理和使用?

核心原理: Sentinel 以流量为切入点,通过滑动窗口统计 QPS/响应时间/异常比例,触发流控/熔断/降级规则。核心是责任链模式 + 滑动窗口。

详细解析:

  • 流控规则
    • QPS 限流:单机每秒请求数超阈值拒绝。
    • 并发线程数限流:同时执行的线程数超阈值拒绝(更适合慢调用场景)。
    • 热点参数限流:按参数值限流(如每个商品 ID 限 100 QPS)。
  • 熔断规则
    • 慢调用比例:响应时间 > 阈值的请求比例超设定值时熔断。
    • 异常比例:异常请求比例超阈值时熔断。
    • 异常数:异常请求数超阈值时熔断。
  • 熔断状态机:CLOSED(正常)→ OPEN(熔断,快速失败)→ HALF_OPEN(半开,放行少量请求探测)→ CLOSED/OPEN。
  • 滑动窗口:Sentinel 将 1 秒分为 2 个 500ms 窗口,滑动统计。LeapArray 数据结构。
  • 规则持久化:默认规则在内存,重启丢失。需配合 Sentinel Dashboard + Nacos 持久化规则。
  • 使用方式@SentinelResource(value = "queryOrder", blockHandler = "blockHandler", fallback = "fallback")

生产场景: 接口需要按用户 ID 限流,VIP 用户配额更高。解决方法: Sentinel 热点参数限流 paramIdx = 0(第一个参数 userId),配置不同参数值不同限流阈值(VIP userId → 1000 QPS,普通 → 100 QPS)。规则存储在 Nacos 动态调整。

Q90:API 网关的作用和设计?

核心原理: API 网关是微服务架构的统一入口,负责路由转发、鉴权、限流、日志、协议转换等横切关注点。

核心功能:

  1. 路由转发:根据 URL/Header 将请求路由到对应微服务。
  2. 统一鉴权:JWT/Token 校验,鉴权通过后转发,未通过返回 401。
  3. 限流熔断:网关层限流(Sentinel),保护后端服务。
  4. 负载均衡:集成 LoadBalancer/Ribbon 负载均衡到多个服务实例。
  5. 协议转换:HTTP → gRPC、WebSocket 代理。
  6. 日志监控:记录请求日志、响应时间、状态码,接入监控。
  7. 灰度发布:按 Header/IP/用户路由到灰度实例。
  8. 跨域处理:CORS 配置。

技术选型:

  • Spring Cloud Gateway:基于 WebFlux 反应式编程,性能高,Spring 生态集成好。
  • Nginx:高性能,Lua 扩展灵活,适合接入层。
  • Kong/APISIX:基于 OpenResty,插件丰富,适合 API 管理。

生产场景: 前端请求需经过鉴权、限流后转发到后端微服务。解决方法: Spring Cloud Gateway + GlobalFilter(JWT 鉴权)+ Sentinel(限流)+ 路由配置(按 path 转发)。灰度发布通过 Header X-Gray=true 路由到灰度实例。

Q91:配置中心的原理和选型?

核心原理: 配置中心统一管理微服务的配置,支持动态刷新(无需重启),核心是配置存储 + 变更通知机制。

核心功能:

  1. 配置集中管理:所有服务的配置存储在配置中心,而非本地文件。
  2. 环境隔离:dev/test/prod 环境配置隔离(namespace/dataId/group)。
  3. 动态刷新:配置变更后实时推送到客户端,@RefreshScope 热更新。
  4. 版本管理:配置变更历史,支持回滚。
  5. 灰度发布:按 IP/标签推送配置到部分实例。

选型对比:

  • Nacos Config:阿里开源,集注册中心+配置中心于一体。长轮询推送(29.5 秒超时),配置变更秒级感知。Spring Cloud Alibaba 首选。
  • Apollo:携程开源,配置管理功能丰富(权限、审批、灰度)。HTTP 长轮询推送,支持多环境。
  • Spring Cloud Config:基于 Git/SVN 存储,Webhook 触发刷新。功能简单,适合小规模。

生产场景: 数据库密码需要动态更新不重启服务。解决方法: Nacos Config + @RefreshScope,配置变更后 Nacos 推送到客户端,@RefreshScope Bean 重新创建。注意 @Value 注解的属性需要 @RefreshScope 才能动态刷新;数据源连接池需手动重建(监听 RefreshEvent 重新创建 DataSource)。

Q92:链路追踪的原理?

核心原理: 链路追踪通过 TraceId(全局唯一)+ SpanId(每段调用唯一)串联一次请求经过的所有服务,核心基于 Google Dapper 论文。

详细解析:

  • 核心概念
    • Trace:一次完整的请求链路,由一个全局唯一的 TraceId 标识。
    • Span:链路中的一段调用(如一次 RPC、一次 DB 查询),有 SpanId 和 ParentSpanId。
    • Annotation:Span 中的事件(cs 客户端发送、sr 服务端接收、ss 服务端发送、cr 客户端接收)。
  • 数据传递:TraceId 和 SpanId 通过 HTTP Header(X-B3-TraceIdX-B3-SpanId)或 MQ 消息属性在服务间传递。
  • 采样率:全量采集影响性能,通常采样 1%-10%。异常请求优先采样。
  • 技术方案
    • SkyWalking:字节码增强(无侵入),Java Agent 方式自动埋点。支持服务拓扑图、慢调用分析。
    • Zipkin:需手动/注解埋点,轻量级。
    • Jaeger:CNCF 项目,OpenTracing 标准。
  • 存储:链路数据量大,通常存 Elasticsearch(查询)或 Cassandra(写入)。

生产场景: 一次请求经过 5 个微服务,响应慢需定位瓶颈。解决方法: SkyWalking Agent 无侵入接入,查看 Trace 链路中每个 Span 的耗时,发现 DB 查询 Span 占 80% 时间,定位到慢 SQL。

Q93:微服务拆分原则?

核心原理: 微服务拆分遵循高内聚、低耦合原则,按业务领域、团队边界、数据自治进行拆分。

拆分原则:

  1. 单一职责(SRP):每个服务负责一个业务领域(订单、商品、用户),不交叉。
  2. 领域驱动设计(DDD):按限界上下文(Bounded Context)拆分,每个上下文对应一个微服务。
  3. 数据自治:每个服务有独立数据库,不直接访问其他服务的数据库(通过 API/RPC 调用)。
  4. 团队自治:一个微服务由一个小团队(3-8 人)负责,遵循康威定律。
  5. 适度拆分:不要过度拆分——服务过多增加运维成本、网络开销、分布式事务复杂度。
  6. 按生命周期拆分:变化频率不同的模块拆开(如核心交易和营销活动)。

拆分策略:

  • 纵向拆分(按业务):用户服务、订单服务、商品服务、支付服务。
  • 横向拆分(按层次):API 网关、BFF(Backend for Frontend)、核心服务。
  • 读写分离:查询服务和写入服务分离(CQRS),查询走 ES/Redis。

生产场景: 单体应用 50 万行代码,20 人团队开发冲突严重。解决方法: 按 DDD 限界上下文拆分为 8 个微服务(用户、商品、订单、支付、库存、营销、物流、通知),每个服务独立数据库、独立部署、独立团队。先拆边界清晰的(通知、营销),后拆核心交易。

Q94:同城多活和异地多活架构?

核心原理: 多活架构是在多个机房部署服务,任一机房故障时其他机房接管流量,核心挑战是数据同步和冲突解决。

详细解析:

  • 同城双活:同城两个机房(延迟 <5ms),双机房同时提供服务,数据库主从跨机房同步。
  • 两地三中心:同城双活 + 异地灾备中心。同城故障切异地,有数据延迟。
  • 异地多活:异地多个机房同时提供服务,数据双向/多向同步。延迟大(10-50ms),需处理数据冲突。

关键技术:

  1. 数据同步:MySQL Binlog + Canal/Kafka 跨机房同步;Redis CRDT/跨机房同步。
  2. 流量调度:DNS/GSLB 按用户地理位置路由到最近机房。
  3. 冲突解决:时间戳/版本号(最后写入获胜)、业务层冲突检测(如用户在两地同时修改同一订单)。
  4. 单元化部署:按用户 ID 路由到固定单元(机房),数据按用户分片,单元内自包含(写本地、读本地)。
  5. 全局协调:分布式 ID(Snowflake)、分布式锁、配置中心需考虑跨机房一致性。

生产场景: 支付系统需机房级容灾,单机房故障不能中断服务。解决方法: 同城双活 + 单元化部署,按用户 ID % 2 路由到机房 A/B,每个机房有完整的用户分片数据。机房故障时 DNS 切换,另一个机房接管全部流量。数据同步延迟 <1 秒。

Q95:灰度发布方案?

核心原理: 灰度发布(金丝雀发布)是逐步将流量从旧版本切换到新版本,降低发布风险。核心是流量分流控制。

常见方案:

  1. 蓝绿发布:两套环境(蓝=旧,绿=新),流量一次性从蓝切换到绿。回滚快(切回蓝),但需双倍资源。
  2. 滚动发布:逐步替换实例(如每次替换 25%),旧实例逐个更新为新版本。资源占用少,但回滚慢。
  3. 金丝雀发布:少量流量(如 5%)先到新版本,观察无异常后逐步扩大(5%→20%→50%→100%)。
  4. A/B 测试:按用户特征分流(如 10% 用户用新功能),对比指标效果。

流量分流方式:

  • 按 Header/Token:请求带 X-Gray=true 路由到灰度实例。
  • 按用户 IDuserId % 100 < 5 的用户走灰度(5% 灰度)。
  • 按权重:网关按权重随机分流(90% 旧版本 + 10% 新版本)。

配合技术:

  • 网关层路由:Spring Cloud Gateway 按 Header/权重路由。
  • 服务注册中心:Nacos 元数据标记灰度实例。
  • 链路追踪:灰度流量标记 TraceId,监控灰度请求的指标。

生产场景: 核心支付服务升级新版本,需小流量验证。解决方法: 金丝雀发布——先发 1 个新版本实例(占总实例 10%),按 Header X-Canary=true 路由内部测试流量,观察 30 分钟无异常后逐步扩量到 30%→50%→100%,最后下线旧版本。异常时网关一键切回 100% 旧版本。

十、场景设计题(Q96-Q108)

Q96:设计一个秒杀系统

核心原理: 秒杀系统的核心挑战是短时间内极高并发、防止超卖、保证系统稳定。核心策略:层层削减流量 + 异步处理 + 原子扣减。

架构设计:

  1. 前端层:CDN 静态化(商品页静态资源)、倒计时按钮防重复点击、前端限流(每秒最多提交一次)。
  2. 网关层:Nginx limit_req 限流、Sentinel 热点参数限流(按用户 ID/IP)。
  3. 应用层
    • 验证码/答题防机器人。
    • Redis SET 预热白名单(仅预约用户可参与)。
    • Redis Lua 原子扣减库存(查库存 → 判断 → 扣减 → 返回结果,一步原子完成)。
  4. 异步层:扣减成功后发 MQ(RocketMQ/Kafka),异步落库创建订单,返回"排队中"。
  5. 数据库层:消费者用 UPDATE stock SET num = num - 1 WHERE id = ? AND num > 0(affected rows=0 则失败)+ 用户+商品唯一索引兜底。
  6. 超时回滚:RocketMQ 延迟消息 15 分钟后检查订单状态,未支付则回滚 Redis + DB 库存。
  7. 防刷风控:同 IP/用户频率限制 + 黑名单 + 设备指纹。

生产场景: 100 件商品,10 万用户同时抢购。解决方法: CDN + Nginx 限流拦截 90% 流量 → Sentinel 限流拦截 5% → Redis Lua 扣减(100 个成功 + 99900 失败快速返回)→ 100 个 MQ 异步落库。DB 压力仅 100 次写入。

Q97:设计订单超时自动关闭功能

核心原理: 订单创建后一定时间内未支付需自动关闭并释放资源(库存、优惠券),核心是延迟任务的实现。

方案对比:

方案 优点 缺点 适用场景
定时任务扫表 简单可靠 精度低(分钟级)、扫描全表慢 小规模
RocketMQ 延迟消息 精度高、解耦 依赖 MQ、固定级别 中大规模
Redis ZSet 轻量灵活 需轮询、宕机丢任务 中小规模
Redisson DelayedQueue 封装优雅 单机性能受限 中小规模
时间轮 高性能、精确 实现复杂 大规模

推荐方案(RocketMQ 延迟消息):

  1. 下单时发送延迟 15 分钟的 RocketMQ 消息,消息体包含订单 ID。
  2. 15 分钟后消费者收到消息,查询订单状态。
  3. 已支付 → 忽略。未支付 → 取消订单 + 释放库存(Redis DECR + DB 更新)+ 解冻优惠券。
  4. 消费端幂等(订单可能已被手动取消),用消息 ID + Redis 去重。

生产场景: 日订单量 100 万,订单 30 分钟未支付自动关闭。解决方法: RocketMQ 延迟消息(30 分钟级别),消费者批量处理(每 10 个订单一批释放库存),消费失败重试 3 次后入死信队列人工处理。定时任务兜底(每小时扫描超时未关闭的订单)。

Q98:如何防止重复支付?

核心原理: 重复支付是用户快速点击或网络重试导致同一订单被支付多次,核心是接口幂等性。

防护方案:

  1. 前端防重:支付按钮点击后禁用 + loading 遮罩,防止用户快速重复点击。
  2. Token 机制:进入支付页时获取一次性 Token,支付请求携带 Token,后端 Lua 原子校验+删除。
  3. 数据库唯一索引:支付流水表对 (order_id, pay_type) 建唯一索引,重复插入抛异常捕获后返回已有支付结果。
  4. 状态机校验UPDATE order SET status='paid' WHERE id=? AND status='unpaid',影响行数=0 表示已支付。
  5. 分布式锁:Redis SETNX 锁住 order_id,支付完成后释放。
  6. 对账机制:定时核对支付流水和订单状态,发现重复支付的自动退款。

生产场景: 用户网络不稳定,支付请求重试 3 次,每次都到达后端。解决方法: 数据库唯一索引 + 状态机双重保障。第一次请求成功扣款 + 状态改为 paid,后续请求 UPDATE 影响行数=0,返回"已支付"而非重复扣款。配合对账系统 T+1 核对。

Q99:设计一个短链接系统

核心原理: 短链接将长 URL 映射为短码(如 t.cn/abc123),核心是短码生成 + 映射存储 + 跳转。

设计方案:

  1. 短码生成
    • 自增 ID + Base62 编码:DB 自增 ID → 转 62 进制(a-z, A-Z, 0-9),6 位可表示 568 亿个。
    • MD5 截取:对长 URL 做 MD5,取前 6-8 位(冲突需重试)。
    • 预生成号段:Leaf-Segment 批量申请 ID 段,本地生成。
  2. 存储:MySQL 存储映射关系(short_code, long_url, expire_time);Redis 缓存热点短码 → 长 URL 映射。
  3. 跳转流程:用户访问短链接 → 查 Redis 缓存(未命中查 DB)→ 302 重定向到长 URL(302 可统计访问次数,301 浏览器缓存不可统计)。
  4. 布隆过滤器:防止恶意请求不存在的短码打穿缓存。
  5. 分库分表:短码数据量大时按 short_code hash 分片。

生产场景: 短链系统日访问量 1 亿,要求 10ms 内跳转。解决方法: Redis 缓存热点短码(TTL 7 天)+ Caffeine 本地缓存(TTL 5 分钟)+ MySQL 兜底。布隆过滤器拦截不存在的短码。302 重定向 + 异步记录访问日志到 Kafka(不影响跳转速度)。

Q100:设计分布式限流系统

核心原理: 分布式限流需要跨多个实例统一计数,核心是集中式计数器 + 原子操作。

设计方案:

  1. Redis + Lua 滑动窗口
    • 用 Redis ZSet,score = 请求时间戳,member = 请求唯一 ID。
    • Lua 脚本:ZREMRANGEBYSCORE key 0 (now - window) 清理过期 → ZCARD key 计数 → 超限则拒绝 → 未超限 ZADD key now uuid → 返回。
    • 原子操作,多实例共享计数。
  2. Sentinel 集群限流:选一个 Token Server 作为限流协调器,各客户端向 Token Server 申请 token,有 token 才放行。
  3. 令牌桶服务:独立服务用令牌桶算法发放 token,各应用从服务获取。

多维度限流:

  • 全局 QPS 限流(保护系统总容量)。
  • 接口级限流(保护单个接口)。
  • 用户级限流(防单用户刷接口)。
  • IP 级限流(防恶意攻击)。

生产场景: API 网关需限制全局 10000 QPS + 每用户 100 QPS + 每 IP 500 QPS。解决方法: Redis + Lua 多 key 限流(global:qpsuser:{uid}:qpsip:{ip}:qps),一次 Lua 脚本同时检查三个维度。Sentinel 集群限流作为备选方案。限流后返回 429 + Retry-After 头。

Q101:如何处理热点数据?

核心原理: 热点数据是访问量远高于普通数据的数据(如热搜商品),可能导致缓存单节点过载、缓存击穿。

处理方案:

  1. 多级缓存:Caffeine 本地缓存(L1)+ Redis(L2),本地缓存兜底减少 Redis 访问。
  2. 热点探测:实时统计 key 访问频率(HotKey 探测工具/Redis --hotkeys),自动识别热点。
  3. 副本分散:热点 key 复制多份(key_1, key_2, ..., key_N),读时随机选副本,分散单 key 压力。
  4. 本地预计算:秒杀等可预知的热点,提前推送到所有节点的本地缓存。
  5. CDN 静态化:热点页面静态化到 CDN,减少后端访问。

生产场景: 明星出轨新闻上热搜,对应文章 key QPS 突增到 10 万,单个 Redis 节点 CPU 100%。解决方法: 热点探测识别 → 自动复制 key 到 10 个副本 → Caffeine 本地缓存 TTL 10 秒 → 10 万 QPS 分散到 10 个 Redis key + 各节点本地缓存,单节点压力降到 1/10。

Q102:设计大文件上传和断点续传

核心原理: 大文件分片上传 + 秒传(MD5 校验)+ 断点续传(记录已上传分片)。

设计方案:

  1. 前端分片:大文件按 5MB 切片,计算文件整体 MD5。
  2. 秒传检查:上传前将 MD5 发送到后端,如果已存在则直接返回成功(秒传)。
  3. 分片上传:逐个/并行上传分片,每个分片带 fileMd5 + chunkIndex + totalChunks
  4. 断点记录:后端记录已上传的分片索引(Redis/DB),中断后前端查询已上传分片,跳过已传的分片继续上传。
  5. 合并分片:全部分片上传完成后,后端合并为完整文件(或用 OSS 的分片上传 API 自动合并)。
  6. 存储:分片存临时目录,合并后存 OSS/MinIO,清理临时分片。

生产场景: 视频网站用户上传 2GB 视频,网络不稳定。解决方法: 前端分 400 个 5MB 分片并行上传(4 并发),每片上传成功记录到 Redis。断网恢复后查询已传 350 片,从 351 片继续。全传完后合并存 OSS。MD5 秒传避免同一文件重复上传。

Q103:设计实时排行榜系统

核心原理: 实时排行榜需要频繁更新分数 + 快速查询排名,Redis ZSet(有序集合)是最优数据结构。

设计方案:

  1. 数据结构ZADD ranking:gameId score userId,score 为积分,member 为用户 ID。
  2. 更新分数ZINCRBY ranking:gameId delta userId,原子增加积分。
  3. 查询 Top NZREVRANGE ranking:gameId 0 9 WITHSCORES,获取前 10 名(降序)。
  4. 查询用户排名ZREVRANK ranking:gameId userId,获取用户在排行榜中的位置。
  5. 分页查询ZREVRANGE ranking:gameId start end WITHSCORES,分页获取排名列表。
  6. 定时快照:每小时将排行榜快照存入 MySQL(历史排名记录)。

性能优化:

  • 百万用户排行榜,ZSet 操作 O(logN) ≈ 20 次比较,毫秒级响应。
  • 分活动/分区域排行榜用不同 key(ranking:activity:123ranking:region:beijing)。
  • 数据量过大(亿级)可分桶(按分数范围分段)。

生产场景: 游戏实时排行榜,100 万玩家,积分实时更新,需展示 Top 100 和个人排名。解决方法: Redis ZSet + ZINCRBY 实时更新 + ZREVRANGE 获取 Top 100 + ZREVRANK 获取个人排名。本地缓存 Top 100(TTL 5 秒)减少 Redis 访问。每小时快照到 MySQL 存历史排名。

Q104:设计"附近的人"功能

核心原理: 基于地理位置(经纬度)查找附近用户,Redis GEO 是最优方案(底层 GeoHash + ZSet)。

设计方案:

  1. 数据存储GEOADD nearby:users longitude latitude userId,存储用户位置。
  2. 查询附近GEORADIUS nearby:users lon lat radius m WITHCOORD WITHDIST COUNT 20 ASC,查询半径内最近的 20 个用户,按距离升序。
  3. 位置更新:用户移动时定期更新位置(GEOADD 覆盖旧位置),设置过期时间清理不活跃用户。
  4. 距离计算GEODIST nearby:users user1 user2 km,计算两用户距离。

优化:

  • 用户位置变化超过一定距离(如 100 米)才更新,减少 Redis 写入。
  • 按城市分 key(nearby:beijingnearby:shanghai),避免单 key 过大。
  • 结合 ZSet 的 TTL 机制清理离线用户。

生产场景: 社交 App 查找附近 1 公里内的在线用户。解决方法: Redis GEO + 定时更新位置(30 秒一次)+ GEORADIUS 查询 1km 内用户。结果中过滤掉已设置"不可被附近的人看到"的用户。离线用户位置 5 分钟过期自动清理。

Q105:设计一个 IM 消息系统

核心原理: IM 系统核心是消息的实时投递(长连接)+ 消息存储 + 消息有序 + 离线消息。

架构设计:

  1. 长连接层:WebSocket/Netty 维持客户端长连接,连接服务器(Connection Server)管理在线连接。
  2. 消息路由:发送者 → Connection Server → 消息路由服务(根据接收者所在 Connection Server 路由)→ 接收者 Connection Server → 推送给接收者。
  3. 消息存储
    • 写扩散:消息写入发送者和每个接收者的收件箱(适合小群)。
    • 读扩散:消息写入会话 timeline,接收者拉取(适合大群)。
    • 存储用 HBase/MongoDB(海量消息历史)。
  4. 消息有序:单会话内消息按序列号(递增 ID)排序,接收方按 seq 去重排序。
  5. 离线消息:用户上线时拉取未读消息(按 seq 对比:客户端最大 seq → 服务端拉取 > seq 的消息)。
  6. 消息可靠性:客户端 ACK 机制(收到消息回 ACK,未 ACK 的重发);消息存库后才算发送成功。
  7. 未读数:Redis 维护每个会话的未读计数。

生产场景: 亿级用户 IM 系统,万人群消息。解决方法: 大群用读扩散(消息写一份到会话 timeline,成员拉取);小群/单聊用写扩散。Netty 长连接 + 消息路由服务 + HBase 存历史消息 + Redis 存未读数 + Kafka 削峰。消息推送走推送中心(兼容 APNs/FCM/厂商通道)。

Q106:设计一个分布式定时任务系统

核心原理: 分布式定时任务需解决:多实例防重复执行、任务分片、失败重试、任务编排。

方案对比:

方案 特点 适用场景
XXL-JOB 中心化调度,可视化,分片广播 通用
Elastic-Job 去中心化(ZK),弹性分片 大规模
Quartz Cluster DB 锁竞争,简单 小规模
PowerJob 支持工作流,MapReduce 分片 复杂任务

XXL-JOB 核心设计:

  1. 调度中心:负责任务调度(定时触发、路由策略、日志管理),自身可集群部署。
  2. 执行器:业务应用嵌入 XXL-JOB Core,注册到调度中心。
  3. 路由策略:第一个/轮询/随机/分片广播/故障转移/忙碌转移。
  4. 分片广播:调度中心将任务分 N 片,每个执行器收到 shardIndex/shardTotal,只处理 hash(dataKey) % shardTotal == shardIndex 的数据。
  5. 失败重试:任务失败自动重试(可配重试次数),超时告警。

生产场景: 每日凌晨处理 1000 万条用户数据,4 台机器并行。解决方法: XXL-JOB 分片广播,shardTotal=4,每台机器处理 250 万条(userId % 4 == shardIndex)。任务失败自动重试 3 次,超过则告警。任务执行日志存调度中心,可视化查看。

Q107:设计一个秒级监控告警系统

核心原理: 监控系统核心链路:指标采集 → 数据传输 → 时序存储 → 查询展示 → 告警通知。

架构设计:

  1. 指标采集
    • JVM/系统指标:Micrometer + Prometheus Client(暴露 /actuator/prometheus)。
    • 业务指标:代码中埋点(Counter/Gauge/Timer)。
    • 日志指标:Filebeat 采集日志 → Logstash 过滤。
  2. 数据传输:Prometheus Pull 模式定期拉取(15 秒);或 Push Gateway 中转短生命周期任务指标。
  3. 时序存储:Prometheus 内置 TSDB(按时间分块存储,默认 15 天);VictoriaMetrics(高性能替代)。
  4. 查询展示:Grafana 连接 Prometheus,配置 Dashboard(QPS、响应时间、错误率、JVM 内存/GC、CPU/内存)。
  5. 告警:Prometheus AlertManager 规则触发(如错误率 > 5% 持续 1 分钟)→ 通知(钉钉/飞书/邮件/电话)。
  6. 链路追踪:SkyWalking 补充全链路视角。

核心指标(RED 原则)

  • Rate:请求速率(QPS)。
  • Errors:错误率(5xx 比例)。
  • Duration:响应时间(P50/P95/P99)。

生产场景: 接口 P99 响应时间突增到 5 秒,需 1 分钟内告警。解决方法: Prometheus 采集 http_server_requests_seconds(P99 > 2 秒持续 1 分钟)→ AlertManager → 钉钉告警 + 自动创建 Jira 工单。Grafana Dashboard 可下钻到慢请求的 SkyWalking Trace 链路定位根因。

Q108:如何设计一个高可用的订单系统?

核心原理: 高可用订单系统需从架构分层、数据可靠、容灾切换、降级兜底四个维度设计,目标是核心链路 99.99% 可用。

架构设计:

  1. 接入层高可用
    • 多机房 DNS 负载均衡 + Nginx 集群(Keepalived VIP)。
    • 网关集群 + Sentinel 限流熔断,防止流量洪峰。
  2. 应用层高可用
    • 微服务多实例部署(至少 3 副本),无状态化设计。
    • 线程池隔离(订单、支付、库存独立线程池),故障不扩散。
    • 超时 + 重试 + 熔断 + 降级(Sentinel),慢依赖快速失败。
  3. 数据层高可用
    • MySQL 主从 + 半同步复制 + MHA/MGR 自动切换。
    • Redis Cluster 高可用 + Sentinel 哨兵。
    • MQ 多副本 + 同步刷盘(RocketMQ)。
  4. 容灾设计
    • 同城双活 + 单元化部署,按用户分片路由。
    • 数据跨机房同步(Canal + Kafka)。
    • 故障自动切换(DNS/网关层切换流量)。
  5. 数据可靠性
    • 订单创建走本地事务 + 本地消息表/MQ 事务消息,保证不丢。
    • 对账系统 T+1 核对订单、支付、库存三方数据。
    • 关键操作记录操作日志(审计追溯)。
  6. 降级兜底
    • 库存查询降级返回缓存数据。
    • 支付降级走异步队列,先接受后处理。
    • 非核心功能(推荐、评论)可降级关闭。

生产场景: 大促期间订单系统需承受 10 万 QPS,99.99% 可用。解决方法: 网关限流 5 万 QPS(超出排队)+ 4 机房单元化部署(每机房 2.5 万 QPS)+ Redis Cluster + MySQL 分库分表(16 库 × 4 表)+ RocketMQ 异步落库 + 全链路监控告警。实测可用性 99.995%,大促零故障。

总结: 本文整理了 108 道 Java 高频面试题,覆盖 Java 基础、并发编程、JVM、MySQL、Redis、消息队列、分布式系统、Spring 生态、微服务架构、场景设计等 10 大方向。每题包含核心原理、详细解析、生产场景与解决方法,适合中高级 Java 工程师面试备考。

参考来源: CSDN、掘金、阿里云开发者社区、腾讯云开发者社区、51CTO、JavaGuide、RocketMQ 官方文档等技术平台公开发布的真实面试题整理。