适用对象:Java 后端开发 / 实习 / 校招
特色:每道题结合我自身一套全栈管理项目编写而来
生成日期:2026-05-28


目录

  1. Java 基础
  2. 集合框架
  3. 多线程与并发
  4. JVM 虚拟机
  5. Spring / Spring Boot
  6. MySQL 数据库
  7. Redis 缓存
  8. MyBatis 持久层
  9. 设计模式
  10. 分布式基础
  11. 场景题 & 综合题
  12. Java 进阶(新增)

1. Java 基础

Q1: == 和 equals() 的区别?

A:

== 比较的是什么?

  • 对于基本类型(int, double, boolean 等):比较的是
  • 对于引用类型(Object, String, List 等):比较的是内存地址(是否是同一个对象)

equals() 比较的是什么?

  • 默认实现(Object 类):和 == 一样,比较内存地址
  • 重写后(如 String, Integer):比较的是内容
// 基本类型:== 比较值
int a = 10;
int b = 10;
System.out.println(a == b);  // true

// 引用类型:== 比较地址
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);      // false(不同对象)
System.out.println(s1.equals(s2)); // true(内容相同)

// 字符串常量池:相同内容的字符串常量指向同一地址
String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4);  // true(常量池中同一对象)

项目场景
在排班算法中,比较两个时间段是否相等:

// ❌ 错误:用 == 比较 Integer 对象
if (timeSlot1.getId() == timeSlot2.getId()) { ... }

// ✅ 正确:用 equals() 比较
if (timeSlot1.getId().equals(timeSlot2.getId())) { ... }

Integer 在 -128~127 范围内有缓存,超出范围后 == 会返回 false。


Q2: String、StringBuilder、StringBuffer 的区别?

A:

特性StringStringBuilderStringBuffer
可变性不可变(final char[])可变可变
线程安全安全(不可变)不安全安全(synchronized)
性能拼接慢(每次创建新对象)最快较慢(加锁开销)
使用场景常量、少量操作单线程大量拼接多线程大量拼接

为什么 String 是不可变的?

public final class String {
    private final char value[];  // final 修饰,不可指向新数组
}
  • final 修饰类:不能被继承
  • final 修饰数组引用:不能指向新数组
  • 但数组内容理论上可以修改(反射可以破坏)

项目场景
在生成操作日志时,需要拼接大量字符串:

// ❌ 低效:每次 += 都会创建新 String 对象
String log = "";
for (OperationLog op : logs) {
    log += op.toString() + "\n";  // O(n²) 时间复杂度
}

// ✅ 高效:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (OperationLog op : logs) {
    sb.append(op.toString()).append("\n");  // O(n) 时间复杂度
}
String log = sb.toString();

Q3: Java 中的异常体系?checked 和 unchecked 异常的区别?

A:

                    Throwable
                   /         \
              Error           Exception
              (不可恢复)       /          \
        OutOfMemoryError  IOException   RuntimeException
        StackOverflowError (checked)     (unchecked)
                            SQLException  NullPointerException
                            FileNotFoundException  ArrayIndexOutOfBoundsException

checked 异常 vs unchecked 异常

类型checked 异常unchecked 异常
父类Exception(非 RuntimeException)RuntimeException
编译期检查必须处理(try-catch 或 throws)不强制处理
典型例子IOException, SQLExceptionNPE, ArrayIndexOutOfBounds
恢复性可恢复(如文件不存在)不可恢复(代码 bug)

项目场景
在调用 Python 服务时,需要处理 checked 异常:

public SalaryPrediction predict(Long userId) {
    try {
        // RestTemplate 调用可能抛出 RestClientException(checked)
        return restTemplate.postForObject(pythonUrl, request, SalaryPrediction.class);
    } catch (RestClientException e) {
        // 降级策略:使用本地公式计算
        log.warn("Python 服务不可用,使用本地计算: {}", e.getMessage());
        return localCalculator.calculate(userId);
    }
}

自定义异常

// 业务异常(unchecked)
public class BusinessException extends RuntimeException {
    private int code;
    
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
}

// 使用
if (user == null) {
    throw new BusinessException(404, "用户不存在");
}

Q4: 接口和抽象类的区别?什么时候用接口,什么时候用抽象类?

A:

特性接口(Interface)抽象类(Abstract Class)
实例化不能不能
构造方法没有
多继承支持(一个类可实现多个接口)不支持(只能继承一个)
方法实现Java 8+ 可以有 default 方法可以有具体方法
成员变量只能是 public static final可以有各种修饰符
设计意图"能做什么"(能力)"是什么"(本质)

什么时候用接口?

  • 定义一组能力/行为,不同类可以有相同能力
  • 需要多继承
  • 解耦,面向接口编程

什么时候用抽象类?

  • 多个子类有共同的属性和方法
  • 需要共享代码实现
  • 需要构造方法初始化

项目场景
排班算法使用策略模式,通过接口实现算法切换:

// 排班算法接口
public interface DutyScheduler {
    List<DutyAssignment> schedule(ScheduleContext context);
}

// V1 版本:贪心算法
@Component("schedulerV1")
public class DutySchedulerV1 implements DutyScheduler {
    @Override
    public List<DutyAssignment> schedule(ScheduleContext context) {
        // 贪心策略实现
    }
}

// V2 版本:五阶段流水线
@Component("schedulerV2")
public class DutySchedulerV2 implements DutyScheduler {
    @Override
    public List<DutyAssignment> schedule(ScheduleContext context) {
        // WSSA 算法实现
    }
}

// 使用时可切换
@Autowired
@Qualifier("schedulerV2")
private DutyScheduler scheduler;

Q5: final、finally、finalize 的区别?

A:

关键字用途示例
final修饰类/方法/变量,表示不可变final class String
finallytry-catch-finally 中的清理代码块关闭资源
finalizeObject 的方法,GC 前调用(已废弃)不推荐使用

final 详解

// 1. 修饰类:不能被继承
final class Constants {
    // ...
}
// class MyConstants extends Constants {}  // 编译错误

// 2. 修饰方法:不能被重写
class Base {
    final void process() { ... }
}
class Sub extends Base {
    // void process() { ... }  // 编译错误
}

// 3. 修饰变量:引用不可变(但对象内容可变)
final List<String> list = new ArrayList<>();
list.add("hello");      // ✅ 可以修改对象内容
// list = new ArrayList<>();  // ❌ 不能改变引用

finally 详解

public String readFile(String path) {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(path));
        return reader.readLine();
    } catch (IOException e) {
        return "error";
    } finally {
        // 无论是否异常,都会执行
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                // 忽略关闭异常
            }
        }
    }
}

// Java 7+ 推荐使用 try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
    return reader.readLine();
}

项目场景
在 SSE 连接管理中,finally 用于清理连接:

public void handleSseConnection(SseEmitter emitter, String userId) {
    try {
        // 发送消息
        emitter.send(data);
    } catch (IOException e) {
        log.error("SSE 发送失败", e);
    } finally {
        // 清理连接
        connectionManager.removeConnection(userId);
    }
}

2. 集合框架

Q6: HashMap 的底层实现原理?JDK 1.8 做了哪些优化?

A:

底层结构:数组 + 链表 + 红黑树

HashMap 内部结构(JDK 1.8):

数组(Node[] table)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │  ← 桶(Bucket)
└─┬─┴───┴─┬─┴───┴───┴─┬─┴───┴───┘
  │       │           │
  ↓       ↓           ↓
┌───┐   ┌───┐       ┌───┐
│ A │   │ B │       │ C │          ← 链表节点
│   │   │   │       │   │
│ ↓ │   │ ↓ │       │ ↓ │
│ D │   │ E │       │ F │
└───┘   └───┘       └───┘

当链表长度 ≥ 8 且数组长度 ≥ 64 时,转为红黑树:

      ┌───┐
      │ G │  ← 红黑树根节点
      └─┬─┘
       / \
    ┌───┐ ┌───┐
    │ H │ │ I │
    └───┘ └───┘

put 流程

  1. 计算 key 的 hash 值:(h = key.hashCode()) ^ (h >>> 16)(高 16 位与低 16 位异或,减少碰撞)
  2. 计算桶位置:index = (n - 1) & hash(n 是数组长度,必须是 2 的幂)
  3. 如果桶为空,直接插入新节点
  4. 如果桶不为空:
    • 如果 key 相同,覆盖 value
    • 如果是红黑树节点,调用红黑树插入
    • 如果是链表,尾插法插入,链表长度 ≥ 8 时转红黑树
  5. 如果 size > capacity * loadFactor,扩容(2 倍)

JDK 1.7 vs 1.8 优化

方面JDK 1.7JDK 1.8
数据结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
扩容先扩容再插入先插入再扩容
hash 计算4 次位运算 + 5 次异或1 次位运算 + 1 次异或
线程安全多线程形成环形链表不会形成环形链表

为什么长度 ≥ 8 才转红黑树?

  • 链表长度符合泊松分布,长度为 8 的概率约为 0.00000006
  • 红黑树节点占用空间是链表节点的 2 倍
  • 链表长度短时,遍历性能可接受
  • 综合考虑空间和时间,8 是一个折中值

项目场景
在排班算法中,使用 HashMap 存储时间段到候选人的映射:

// 时间段 → 候选人列表
Map<Integer, List<Student>> slotCandidates = new HashMap<>();

// 预处理:将空课数据填入 HashMap
for (Course course : courses) {
    int slot = course.getTimeSlot();
    // computeIfAbsent:如果 key 不存在,创建新 List
    slotCandidates.computeIfAbsent(slot, k -> new ArrayList<>())
                  .add(course.getStudent());
}

Q7: ConcurrentHashMap 如何保证线程安全?

A:

JDK 1.7:分段锁(Segment)

ConcurrentHashMap(1.7)

┌─────────────────────────────────────┐
│  Segment[0]  │  Segment[1]  │ ...  │
│  ┌────────┐  │  ┌────────┐  │      │
│  │ HashEntry│  │ HashEntry│  │      │
│  │ 数组     │  │ 数组     │  │      │
│  └────────┘  │  └────────┘  │      │
└─────────────────────────────────────┘

每个 Segment 是一个独立的 HashMap,有自己的锁
并发度 = Segment 数量(默认 16)

JDK 1.8:CAS + synchronized

ConcurrentHashMap(1.8)

┌─────────────────────────────────────┐
│  Node[] 数组                         │
│  ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│  │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ │
│  └─┬─┴───┴─┬─┴───┴───┴─┬─┴───┴───┘ │
│    │       │           │            │
│    ↓       ↓           ↓            │
│  链表/红黑树(锁住链表头节点)          │
└─────────────────────────────────────┘

put 流程(JDK 1.8)

  1. 计算 hash,找到桶位置
  2. 如果桶为空,使用 CAS 插入新节点(无锁)
  3. 如果桶不为空,使用 synchronized 锁住链表头节点
  4. 遍历链表,如果 key 存在则覆盖,否则尾插法插入
  5. 链表长度 ≥ 8 时转红黑树
  6. baseCount + CounterCell[] 分散计数(类似 LongAdder)

为什么 JDK 1.8 放弃分段锁?

  • 分段锁的锁粒度是 Segment,一个 Segment 可能包含多个桶
  • JDK 1.8 的锁粒度是单个桶(Node),并发度更高
  • CAS + synchronized 的组合比 ReentrantLock 更轻量

项目场景
在 SSE 连接管理中,使用 ConcurrentHashMap 存储连接:

public class SseConnectionManager {
    // 线程安全的连接存储
    private final ConcurrentHashMap<String, SseEmitter> connections = new ConcurrentHashMap<>();
    
    public void addConnection(String userId, SseEmitter emitter) {
        connections.put(userId, emitter);  // CAS 保证线程安全
    }
    
    public void removeConnection(String userId) {
        connections.remove(userId);
    }
    
    public void broadcast(String message) {
        // 遍历时不需要加锁,ConcurrentHashMap 支持并发读
        connections.forEach((userId, emitter) -> {
            try {
                emitter.send(message);
            } catch (IOException e) {
                removeConnection(userId);
            }
        });
    }
}

Q8: ArrayList 和 LinkedList 的区别?什么时候用哪个?

A:

特性ArrayListLinkedList
底层结构动态数组(Object[])双向链表
随机访问O(1)(通过下标)O(n)(需要遍历)
头部插入O(n)(需要移动元素)O(1)
尾部插入O(1)(均摊)O(1)
内存占用连续内存,空间利用率高每个节点额外存储前后指针
扩容1.5 倍扩容,需要复制数组无需扩容

ArrayList 扩容机制

// 初始容量 10,每次扩容 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);  // 右移 1 位 = 除以 2

// 扩容过程:
// 1. 创建新数组(1.5 倍大小)
// 2. 使用 Arrays.copyOf() 复制元素
// 3. 旧数组被 GC 回收

使用场景

// ✅ 场景1:需要随机访问 → ArrayList
List<User> users = userMapper.selectAll();
User user = users.get(100);  // O(1) 快速访问

// ✅ 场景2:频繁头部插入 → LinkedList
LinkedList<Task> taskQueue = new LinkedList<>();
taskQueue.addFirst(new Task());  // O(1) 头部插入
Task task = taskQueue.pollFirst();  // O(1) 头部取出

// ✅ 场景3:只读列表 → Collections.unmodifiableList()
List<String> constants = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));

项目场景
在排班算法的候选人队列中:

// Phase 1:按稀缺度排序时间段(频繁排序,用 ArrayList)
List<TimeSlot> slots = new ArrayList<>(slotCandidates.keySet());
slots.sort(Comparator.comparingInt(slot -> slotCandidates.get(slot).size()));

// Phase 2:候选人评分排序(频繁插入删除,用 LinkedList)
LinkedList<Student> candidates = new LinkedList<>();
for (Student s : slotCandidates.get(slot)) {
    // 按分数插入到正确位置(保持有序)
    insertSorted(candidates, s, scoreMap);
}

Q9: Iterator 和 ListIterator 的区别?fail-fast 和 fail-safe 机制?

A:

Iterator vs ListIterator

特性IteratorListIterator
适用集合所有 Collection只能用于 List
遍历方向单向(从前往后)双向(前后都可以)
方法hasNext(), next(), remove()额外有 hasPrevious(), previous(), add(), set()

fail-fast 机制

  • 原理:集合维护一个 modCount(修改计数器),每次结构性修改(增删)都会 +1
  • 检测:迭代器在创建时记录 expectedModCount,每次操作前检查是否相等
  • 触发:如果在迭代过程中直接修改集合(不通过迭代器),会抛出 ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// ❌ 错误:直接修改集合,触发 fail-fast
for (String s : list) {
    if (s.equals("B")) {
        list.remove(s);  // ConcurrentModificationException!
    }
}

// ✅ 正确:使用迭代器的 remove 方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("B")) {
        it.remove();  // 安全删除
    }
}

fail-safe 机制

  • 原理:遍历的是集合的副本或快照,修改不影响迭代
  • 实现CopyOnWriteArrayListConcurrentHashMap
  • 缺点:无法反映最新的修改
// fail-safe 示例:CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));

for (String s : list) {
    if (s.equals("B")) {
        list.remove(s);  // 不会抛异常,但迭代的是旧快照
    }
}

项目场景
在清理过期的 SSE 连接时,需要安全遍历:

// ❌ 错误:直接遍历删除
for (Map.Entry<String, SseEmitter> entry : connections.entrySet()) {
    if (isExpired(entry.getValue())) {
        connections.remove(entry.getKey());  // ConcurrentModificationException!
    }
}

// ✅ 正确:使用 ConcurrentHashMap 的 removeIf 或迭代器
connections.entrySet().removeIf(entry -> isExpired(entry.getValue()));

// 或者使用 Iterator
Iterator<Map.Entry<String, SseEmitter>> it = connections.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, SseEmitter> entry = it.next();
    if (isExpired(entry.getValue())) {
        it.remove();  // 安全删除
    }
}

3. 多线程与并发

Q10: synchronized 和 ReentrantLock 的区别?

A:

特性synchronizedReentrantLock
实现层面JVM 层面(monitorenter/monitorexit)JDK 层面(java.util.concurrent)
锁获取自动获取和释放手动 lock() 和 unlock()
可中断不可中断可中断(lockInterruptibly())
超时机制支持(tryLock(timeout))
公平锁非公平支持公平和非公平
条件变量只有一个 wait/notify支持多个 Condition
性能JDK 6+ 优化后差不多高竞争下略优

synchronized 的三种用法

// 1. 修饰实例方法:锁住当前对象
public synchronized void method() { ... }

// 2. 修饰静态方法:锁住 Class 对象
public static synchronized void staticMethod() { ... }

// 3. 修饰代码块:锁住指定对象
public void method() {
    synchronized (this) { ... }  // 锁住当前对象
    synchronized (MyClass.class) { ... }  // 锁住 Class 对象
}

ReentrantLock 的使用

private final ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();  // 手动加锁
    try {
        // 临界区代码
    } finally {
        lock.unlock();  // 必须在 finally 中释放锁
    }
}

公平锁 vs 非公平锁

// 非公平锁(默认):新来的线程可能插队
ReentrantLock unfairLock = new ReentrantLock(false);

// 公平锁:按请求顺序获取锁
ReentrantLock fairLock = new ReentrantLock(true);

项目场景
在排班算法中,需要保证同一时间只有一个排班任务执行:

@Service
public class DutySchedulerService {
    private final ReentrantLock schedulerLock = new ReentrantLock();
    
    public ScheduleResult schedule(ScheduleRequest request) {
        // 尝试获取锁,最多等待 5 秒
        if (!schedulerLock.tryLock(5, TimeUnit.SECONDS)) {
            throw new BusinessException(500, "排班任务正在执行中,请稍后重试");
        }
        
        try {
            // 执行排班算法(可能耗时较长)
            return dutyScheduler.schedule(request);
        } finally {
            schedulerLock.unlock();
        }
    }
}

Q11: volatile 关键字的作用?它能保证原子性吗?

A:

volatile 的两个作用

1. 保证可见性

线程1 修改 volatile 变量
       ↓
写入主内存(强制刷新)
       ↓
线程2 读取时看到最新值(强制从主内存读取)

2. 禁止指令重排序

// 经典案例:双重检查锁定(DCL)单例模式
public class Singleton {
    private static volatile Singleton instance;  // 必须用 volatile
    
    public static Singleton getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查
                    instance = new Singleton();  // 非原子操作!
                }
            }
        }
        return instance;
    }
}

为什么 DCL 必须用 volatile?
new Singleton() 实际上是三步操作:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

JVM 可能将 2 和 3 重排序为 1→3→2,导致其他线程看到未初始化的对象。volatile 禁止重排序。

volatile 不能保证原子性

private volatile int count = 0;

// ❌ 这不是原子操作!
// 实际上是三步:读取 count → 加 1 → 写入 count
count++;  // 多线程下会丢失更新

// ✅ 解决方案1:使用 AtomicInteger
private AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();  // CAS 保证原子性

// ✅ 解决方案2:使用 synchronized
public synchronized void increment() {
    count++;
}

项目场景
在 WebSocket 连接管理中,使用 volatile 标记连接状态:

public class WebSocketHandler {
    private volatile boolean connected = false;  // 保证多线程可见
    
    public void onOpen() {
        connected = true;
    }
    
    public void onClose() {
        connected = false;
    }
    
    public void sendMessage(String message) {
        if (!connected) {  // 多线程读取时能看到最新状态
            throw new IllegalStateException("WebSocket 未连接");
        }
        // 发送消息...
    }
}

Q12: ThreadLocal 的原理?存在什么问题?

A:

ThreadLocal 的原理

每个 Thread 对象内部都有一个 ThreadLocalMap:

Thread 对象
├── threadLocals: ThreadLocalMap
│   ├── Entry[0]: key=ThreadLocal对象(弱引用), value=实际值
│   ├── Entry[1]: key=ThreadLocal对象(弱引用), value=实际值
│   └── ...
├── threadLocalHashCode: 4324324
└── ...

ThreadLocal 对象本身不存储数据,只是一个 key
数据存储在当前线程的 ThreadLocalMap 中

get() 流程

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;  // 获取当前线程的 Map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  // 用 this(ThreadLocal)作为 key
        if (e != null) {
            return (T)e.value;
        }
    }
    return setInitialValue();  // 初始化默认值
}

内存泄漏问题

ThreadLocalMap 的 Entry 结构:

Entry {
    WeakReference<ThreadLocal<?>> key;  // 弱引用!
    Object value;                        // 强引用!
}

问题场景:
1. ThreadLocal 对象被 GC 回收(没有强引用指向它)
2. Entry 的 key 变成 null
3. 但 value 仍然被 Entry 强引用,无法回收
4. 如果线程是线程池中的核心线程,永远不会销毁
5. 导致 value 永远无法回收 → 内存泄漏!

解决方案:使用完毕后必须调用 remove() 方法

private static final ThreadLocal<User> userContext = new ThreadLocal<>();

public void processRequest(HttpServletRequest request) {
    try {
        User user = getUserFromToken(request);
        userContext.set(user);  // 设置用户上下文
        
        // 业务逻辑...
        doSomething();
        
    } finally {
        userContext.remove();  // 必须清理!防止内存泄漏
    }
}

项目场景
项目中 BaseContext 使用 ThreadLocal 存储当前请求的用户信息:

public class BaseContext {
    private static final ThreadLocal<Long> userId = new ThreadLocal<>();
    private static final ThreadLocal<String> userName = new ThreadLocal<>();
    
    public static void setCurrentId(Long id) {
        userId.set(id);
    }
    
    public static Long getCurrentId() {
        return userId.get();
    }
    
    // 清理方法(在拦截器中调用)
    public static void clear() {
        userId.remove();
        userName.remove();
    }
}

// 在 JWT 拦截器中使用
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, ...) {
        String token = request.getHeader("token");
        Claims claims = JwtUtil.parseToken(token);
        BaseContext.setCurrentId(claims.get("userId", Long.class));
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, ...) {
        BaseContext.clear();  // 请求结束后清理,防止内存泄漏
    }
}

Q13: 线程池的核心参数?如何合理配置?

A:

ThreadPoolExecutor 的 7 个核心参数

public ThreadPoolExecutor(
    int corePoolSize,       // 核心线程数(常驻线程)
    int maximumPoolSize,    // 最大线程数
    long keepAliveTime,     // 非核心线程的空闲存活时间
    TimeUnit unit,          // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

线程池工作流程

提交任务
    ↓
当前线程数 < corePoolSize?
    ├── 是 → 创建核心线程执行
    └── 否 → 任务队列未满?
              ├── 是 → 放入队列等待
              └── 否 → 当前线程数 < maximumPoolSize?
                        ├── 是 → 创建非核心线程执行
                        └── 否 → 执行拒绝策略

四种拒绝策略
| 策略 | 行为 |
|------|------|
| AbortPolicy(默认) | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 由调用线程执行任务 |
| DiscardPolicy | 静默丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 |

如何合理配置线程数?

// CPU 密集型:线程数 = CPU 核心数 + 1
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
    cpuCores + 1,
    cpuCores + 1,
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);

// IO 密集型:线程数 = CPU 核心数 * 2
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    cpuCores * 2,
    cpuCores * 2,
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);

项目场景
项目中使用线程池异步写入操作日志:

@Configuration
public class AsyncConfig {
    @Bean("operationLogExecutor")
    public Executor operationLogExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);           // 核心线程数
        executor.setMaxPoolSize(5);            // 最大线程数
        executor.setQueueCapacity(1000);       // 队列容量
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("op-log-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

// 在 AOP 切面中使用
@Aspect
@Component
public class OperationLogAspect {
    @Async("operationLogExecutor")  // 异步执行,不阻塞业务线程
    public void saveLog(OperationLog log) {
        operationLogMapper.insert(log);
    }
}

Q14: CountDownLatch 和 CyclicBarrier 的区别?

A:

特性CountDownLatchCyclicBarrier
计数方向递减到 0递增到目标值
可重用不可重用(一次性)可重用(reset)
等待方一个或多个线程等待所有线程互相等待
触发点countDown() 到 0 时所有线程都到达屏障点
典型场景主线程等待子任务完成多线程同步开始

CountDownLatch 示例

// 主线程等待 3 个子任务完成
CountDownLatch latch = new CountDownLatch(3);

// 子线程1
new Thread(() -> {
    doTask1();
    latch.countDown();  // 计数 -1
}).start();

// 子线程2
new Thread(() -> {
    doTask2();
    latch.countDown();
}).start();

// 子线程3
new Thread(() -> {
    doTask3();
    latch.countDown();
}).start();

// 主线程等待所有子任务完成
latch.await();  // 阻塞直到计数为 0
System.out.println("所有子任务完成");

CyclicBarrier 示例

// 3 个线程同步开始
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程都到达屏障点,开始下一阶段");
});

// 线程1
new Thread(() -> {
    preparePhase1();
    barrier.await();  // 等待其他线程
    startPhase2();
}).start();

// 线程2、3 类似...

项目场景
在排班算法的多阶段处理中,可以使用 CountDownLatch 等待所有阶段完成:

public ScheduleResult schedule(ScheduleContext context) {
    CountDownLatch latch = new CountDownLatch(3);
    
    // Phase 1:数据预处理(异步)
    CompletableFuture.runAsync(() -> {
        preprocessData(context);
        latch.countDown();
    });
    
    // Phase 2:评分计算(异步)
    CompletableFuture.runAsync(() -> {
        calculateScores(context);
        latch.countDown();
    });
    
    // Phase 3:约束校验(异步)
    CompletableFuture.runAsync(() -> {
        validateConstraints(context);
        latch.countDown();
    });
    
    // 等待所有阶段完成
    latch.await(30, TimeUnit.SECONDS);
    
    // Phase 4:结果合并(同步)
    return mergeResults(context);
}

4. JVM 虚拟机

Q15: JVM 的内存结构?堆和栈的区别?

A:

JVM 运行时数据区

┌─────────────────────────────────────────────────────────┐
│                    JVM 内存结构                          │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  线程私有                                          │   │
│   │  ┌──────────┐  ┌──────────┐  ┌──────────┐       │   │
│   │  │ 程序计数器 │  │ 虚拟机栈  │  │ 本地方法栈 │       │   │
│   │  │ (PC)      │  │ (Stack)  │  │ (Native) │       │   │
│   │  └──────────┘  └──────────┘  └──────────┘       │   │
│   └─────────────────────────────────────────────────┘   │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  线程共享                                          │   │
│   │  ┌──────────────────────────────────────────┐    │   │
│   │  │  堆 (Heap)                                 │    │   │
│   │  │  ┌──────────────┐  ┌──────────────┐      │    │   │
│   │  │  │  新生代        │  │  老年代        │      │    │   │
│   │  │  │ Eden│S0│S1   │  │  Old Gen     │      │    │   │
│   │  │  └──────────────┘  └──────────────┘      │    │   │
│   │  └──────────────────────────────────────────┘    │   │
│   │  ┌──────────────────────────────────────────┐    │   │
│   │  │  方法区 / 元空间 (Metaspace)                │    │   │
│   │  └──────────────────────────────────────────┘    │   │
│   └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

堆 vs 栈

特性堆 (Heap)栈 (Stack)
存储内容对象实例、数组局部变量、方法调用
共享性线程共享线程私有
生命周期GC 管理方法结束自动释放
大小较大(-Xmx 配置)较小(-Xss 配置,默认 1M)
异常OutOfMemoryErrorStackOverflowError

对象创建过程

1. 类加载检查:检查类是否已加载、解析、初始化
2. 分配内存:
   - 指针碰撞(内存规整时)
   - 空闲列表(内存不规整时)
3. 初始化零值:将内存空间初始化为 0
4. 设置对象头:Mark Word(哈希码、GC 分代年龄、锁状态)
5. 执行 <init>:执行构造方法

项目场景
项目中创建大量排班结果对象时,需要注意堆内存:

// ❌ 低效:循环中创建大量临时对象
for (int i = 0; i < 10000; i++) {
    String temp = new String("temp_" + i);  // 每次都创建新对象
}

// ✅ 高效:使用 StringBuilder 或对象池
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);  // 复用
    sb.append("temp_").append(i);
    String temp = sb.toString();
}

Q16: 垃圾回收算法有哪些?如何选择垃圾收集器?

A:

四种基础垃圾回收算法

算法原理优点缺点
标记-清除标记存活对象,清除未标记简单内存碎片
标记-整理标记存活对象,向一端移动无碎片移动开销大
复制将存活对象复制到另一块内存无碎片,高效空间利用率 50%
分代收集新生代用复制,老年代用标记-整理综合最优实现复杂

常见垃圾收集器

收集器区域算法特点
Serial新生代复制单线程,简单高效
ParNew新生代复制Serial 的多线程版本
Parallel Scavenge新生代复制关注吞吐量
CMS老年代标记-清除低停顿,但有碎片
G1全堆分区 + 复制/整理JDK 9+ 默认,可控停顿
ZGC全堆染色指针超低停顿(< 10ms)

G1 收集器的特点

G1 将堆划分为多个 Region(默认 2048 个):

┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ S │ O │ O │ H │   │   │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ O │   │ E │ O │ H │   │ E │   │
└───┴───┴───┴───┴───┴───┴───┴───┘

E = Eden    S = Survivor    O = Old    H = Humongous(大对象)
空闲 = 未使用

G1 会优先回收垃圾最多的 Region(Garbage First 名称由来)

JVM 参数配置示例

# 使用 G1 收集器,最大堆 2G,目标停顿 200ms
java -XX:+UseG1GC \
     -Xmx2g \
     -Xms2g \
     -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=4m \
     -jar app.jar

项目场景
计算中心系统部署时的 JVM 配置:

# 生产环境配置
java -XX:+UseG1GC \
     -Xmx1g \
     -Xms1g \
     -XX:MaxGCPauseMillis=200 \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/logs/heapdump.hprof \
     -jar cc-server.jar

Q17: 如何排查 OOM 问题?

A:

OOM 的常见类型
| 类型 | 原因 | 场景 |
|------|------|------|
| Java heap space | 堆内存不足 | 内存泄漏、大对象 |
| Metaspace | 类元数据区不足 | 动态生成大量类 |
| GC overhead limit exceeded | GC 耗时过长 | 内存接近满 |
| Direct buffer memory | 直接内存不足 | NIO 未释放 |
| Unable to create new native thread | 线程数超限 | 线程泄漏 |

排查步骤

第一步:获取堆转储

# 方式1:启动参数自动导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof

# 方式2:jmap 手动导出
jmap -dump:format=b,file=heapdump.hprof <pid>

# 方式3:arthas 在线导出
heapdump /logs/heapdump.hprof

第二步:分析堆转储
使用 MAT(Memory Analyzer Tool)或 VisualVM:

  1. 打开 heapdump.hprof
  2. 查看 Dominator Tree(支配树),找到占用内存最大的对象
  3. 查看 Leak Suspects(泄漏嫌疑人),MAT 自动分析

第三步:定位代码

# 查看 GC 日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log

# 使用 jstat 查看 GC 情况
jstat -gcutil <pid> 1000 10

常见 OOM 场景及解决

// 场景1:集合类持有对象引用未释放
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]);  // 不断添加,永不释放
}

// 解决:及时清理
list.clear();
list = null;

// 场景2:ThreadLocal 未清理
private static ThreadLocal<User> userContext = new ThreadLocal<>();
public void process() {
    userContext.set(getUser());
    // 业务逻辑...
    // ❌ 忘记清理
}

// 解决:在 finally 中清理
public void process() {
    try {
        userContext.set(getUser());
        // 业务逻辑...
    } finally {
        userContext.remove();  // ✅ 必须清理
    }
}

// 场景3:数据库连接未关闭
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// ❌ 忘记关闭

// 解决:使用 try-with-resources
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    // 处理结果
}

项目场景
项目中使用 HikariCP 连接池,配置泄漏检测:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      leak-detection-threshold: 30000  # 30 秒未关闭连接则告警

5. Spring / Spring Boot

Q18: Spring Boot 的自动配置原理?

A:

核心注解

@SpringBootApplication
    ├── @SpringBootConfiguration
    ├── @EnableAutoConfiguration
    │       └── @Import(AutoConfigurationImportSelector.class)
    └── @ComponentScan

自动配置流程

1. @EnableAutoConfiguration 触发自动配置
          ↓
2. AutoConfigurationImportSelector 加载配置类
          ↓
3. 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
          ↓
4. 根据条件注解过滤配置类
          ↓
5. 满足条件的配置类生效

条件注解

@ConditionalOnClass(DataSource.class)         // 类路径存在 DataSource
@ConditionalOnMissingBean(DataSource.class)   // 容器中没有 DataSource Bean
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")  // 配置了数据源 URL

示例:Redis 自动配置

// 引入 spring-boot-starter-data-redis 后,自动配置生效
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)  // 类路径有 RedisOperations
@EnableConfigurationProperties(RedisProperties.class)  // 绑定配置
public class RedisAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")  // 用户没自定义才生效
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }
}

项目场景
项目中自定义 Redis 配置,覆盖自动配置:

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 自定义序列化方式
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        serializer.setObjectMapper(mapper);
        
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.afterPropertiesSet();
        
        return template;
    }
}

Q19: Spring IOC 和 AOP 的理解?

A:

IOC(控制反转)

传统方式:
    A 需要 B → A 自己创建 B → A 强依赖 B

IOC 方式:
    A 需要 B → 容器创建 B → 容器注入 B → A 不关心 B 怎么创建
    
控制反转:对象的创建和依赖管理从程序转移到容器
依赖注入:容器将依赖对象注入到需要的地方

IOC 的三种注入方式

// 1. 构造器注入(推荐)
@Service
public class UserService {
    private final UserMapper userMapper;
    
    @Autowired  // 可省略
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

// 2. Setter 注入
@Service
public class UserService {
    private UserMapper userMapper;
    
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

// 3. 字段注入(不推荐)
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
}

为什么推荐构造器注入?

  • 可以用 final 修饰,保证不可变
  • 依赖明确,便于单元测试
  • 避免循环依赖

AOP(面向切面编程)

AOP 核心概念:

1. 切面(Aspect):横切关注点的模块化(如日志、事务)
2. 连接点(JoinPoint):程序执行的某个点(如方法调用)
3. 切入点(Pointcut):匹配连接点的表达式
4. 通知(Advice):在切入点执行的动作
   - @Before:前置通知
   - @After:后置通知
   - @AfterReturning:返回通知
   - @AfterThrowing:异常通知
   - @Around:环绕通知
5. 织入(Weaving):将切面应用到目标对象的过程

项目场景
项目中使用 AOP 实现操作日志和自动填充:

// 操作日志切面
@Aspect
@Component
public class OperationLogAspect {
    
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            
            // 记录成功日志
            saveLog(joinPoint, result, null, System.currentTimeMillis() - startTime);
            
            return result;
        } catch (Throwable e) {
            // 记录失败日志
            saveLog(joinPoint, null, e, System.currentTimeMillis() - startTime);
            throw e;
        }
    }
}

// 使用
@PostMapping("/admin/user")
@OperationLog("新增用户")
public Result addUser(@RequestBody UserDTO userDTO) {
    userService.addUser(userDTO);
    return Result.success();
}

Q20: @Transactional 事务失效的常见场景?

A:

六种常见失效场景

1. 方法非 public

// ❌ 事务失效:Spring AOP 只代理 public 方法
@Transactional
private void updateUser() { ... }

// ✅ 正确
@Transactional
public void updateUser() { ... }

2. 自调用

@Service
public class UserService {
    
    public void methodA() {
        this.methodB();  // ❌ 直接调用,绕过了代理对象
    }
    
    @Transactional
    public void methodB() { ... }
}

// 解决方案1:注入自身
@Autowired
private UserService userService;

public void methodA() {
    userService.methodB();  // ✅ 通过代理对象调用
}

// 解决方案2:使用 AopContext
public void methodA() {
    ((UserService) AopContext.currentProxy()).methodB();
}

3. 异常类型不匹配

// ❌ 事务失效:默认只回滚 RuntimeException 和 Error
@Transactional
public void updateUser() throws Exception {
    // ...
    throw new Exception("checked exception");  // 不回滚!
}

// 解决:指定回滚异常
@Transactional(rollbackFor = Exception.class)
public void updateUser() throws Exception { ... }

4. 异常被吞掉

// ❌ 事务失效:catch 吞掉了异常
@Transactional
public void updateUser() {
    try {
        // 业务逻辑
        userMapper.update(user);
    } catch (Exception e) {
        log.error("更新失败", e);  // 异常被吞掉,事务管理器感知不到
    }
}

// ✅ 正确:不吞异常,或手动回滚
@Transactional
public void updateUser() {
    try {
        userMapper.update(user);
    } catch (Exception e) {
        log.error("更新失败", e);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();  // 手动回滚
    }
}

5. 数据库引擎不支持

-- ❌ MyISAM 不支持事务
CREATE TABLE user (...) ENGINE=MyISAM;

-- ✅ InnoDB 支持事务
CREATE TABLE user (...) ENGINE=InnoDB;

6. 传播行为配置错误

// ❌ REQUIRES_NEW 导致内层事务独立提交
@Transactional
public void methodA() {
    userMapper.update(user1);
    methodB();  // 内层事务独立提交
    throw new RuntimeException("外层异常");  // 只回滚外层
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    userMapper.update(user2);  // 已经提交,不会回滚
}

项目场景
项目中工资计算需要事务保证:

@Service
public class SalaryService {
    
    @Transactional(rollbackFor = Exception.class)
    public void calculateAndSaveSalary(SalaryDTO dto) {
        // 1. 查询值班数据
        List<DutyRecord> records = dutyMapper.selectByUserId(dto.getUserId());
        
        // 2. 计算工资
        Salary salary = calculateSalary(records);
        
        // 3. 保存工资记录
        salaryMapper.insert(salary);
        
        // 4. 更新用户工资状态
        userMapper.updateSalaryStatus(dto.getUserId(), 1);
        
        // 如果任何一步异常,全部回滚
    }
}

6. MySQL 数据库

Q21: MySQL 索引的底层数据结构?为什么用 B+ 树?

A:

为什么不用其他数据结构?

数据结构为什么不合适
二叉搜索树可能退化为链表,O(n)
AVL/红黑树树高太高,IO 次数多
Hash不支持范围查询、排序
B 树非叶子节点存数据,单节点存的 key 少

B+ 树的优势

B+ 树结构(3 层可存约 2000 万条数据):

                    ┌─────────────┐
                    │  50 │ 100   │      ← 根节点(不存数据)
                    └───────┬─────┘
                   /        │        \
         ┌────────┐   ┌────────┐   ┌────────┐
         │20│30│40│   │60│70│80│   │110│120  │  ← 非叶子节点(不存数据)
         └──┬───┘   └──┬───┘   └──┬───┘
           / \        / \        / \
     ┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐
     │数据││数据││数据││数据││数据││数据│  ← 叶子节点(存数据)
     └───┘└───┘└───┘└───┘└───┘└───┘
       ↑    ↑    ↑    ↑    ↑    ↑
       └────┴────┴────┴────┴────┘
              双向链表连接

B+ 树的三大优势

  1. 矮胖结构:每个节点可存多个 key,3 层可存 2000 万数据,IO 次数少
  2. 叶子节点链表:天然支持范围查询(WHERE age BETWEEN 20 AND 30
  3. 非叶子节点不存数据:单个节点可容纳更多 key,进一步降低树高

项目场景
项目中为用户表建立索引:

-- 主键索引(聚簇索引)
ALTER TABLE user ADD PRIMARY KEY (id);

-- 唯一索引
ALTER TABLE user ADD UNIQUE KEY uk_username (username);

-- 普通索引(加速登录查询)
ALTER TABLE user ADD INDEX idx_stu_id (stu_id);

-- 联合索引(覆盖索引)
ALTER TABLE user ADD INDEX idx_name_username (name, username);

Q22: 聚簇索引和非聚簇索引的区别?什么是回表?

A:

聚簇索引(主键索引)

叶子节点存储完整的行数据:

        ┌─────────────┐
        │  主键索引     │
        └───────┬─────┘
               / \
        ┌─────┐   ┌─────┐
        │完整行│   │完整行│   ← 叶子节点存储完整数据
        │数据  │   │数据  │
        └─────┘   └─────┘

非聚簇索引(二级索引)

叶子节点存储主键值:

        ┌─────────────┐
        │  二级索引     │
        └───────┬─────┘
               / \
        ┌─────┐   ┌─────┐
        │主键值│   │主键值│   ← 叶子节点只存主键
        └─────┘   └─────┘

回表过程

查询:SELECT * FROM user WHERE username = 'wangwenjia'

1. 在 username 二级索引中找到 username='wangwenjia' 的记录
2. 获取对应的主键值(如 id=100)
3. 回到主键索引,用 id=100 查找完整行数据

        二级索引                    主键索引
        ┌─────────┐               ┌─────────┐
        │username │               │   id    │
        │ wangwenjia │  ──────────→ │   100   │
        │ id=100  │               │ 完整数据 │
        └─────────┘               └─────────┘

覆盖索引(避免回表)

-- 建立联合索引
ALTER TABLE user ADD INDEX idx_username_name (username, name);

-- 覆盖索引查询:只需要 username 和 name,不用回表
SELECT username, name FROM user WHERE username = 'wangwenjia';

-- 解释:Extra 列显示 Using index,表示使用了覆盖索引
EXPLAIN SELECT username, name FROM user WHERE username = 'wangwenjia';

项目场景
项目中查询用户列表时使用覆盖索引:

// 需求:查询用户列表,只需要 id、username、name
@Select("SELECT id, username, name FROM user WHERE status = 1")
List<UserVO> selectUserList();

// 优化:建立联合索引
// ALTER TABLE user ADD INDEX idx_status_username_name (status, username, name);

Q23: MySQL 事务的隔离级别?MVCC 原理?

A:

四种隔离级别

隔离级别脏读不可重复读幻读说明
READ UNCOMMITTED最低,几乎不用
READ COMMITTEDOracle 默认
REPEATABLE READMySQL 默认
SERIALIZABLE最高,性能差

脏读、不可重复读、幻读

脏读:读到其他事务未提交的数据
  T1: UPDATE balance = 100 (未提交)
  T2: SELECT balance → 100  (脏读!)
  T1: ROLLBACK

不可重复读:同一事务内两次读取结果不同
  T1: SELECT balance → 100
  T2: UPDATE balance = 200, COMMIT
  T1: SELECT balance → 200  (不可重复读!)

幻读:同一事务内两次查询的记录数不同
  T1: SELECT COUNT(*) → 10
  T2: INSERT INTO ..., COMMIT
  T1: SELECT COUNT(*) → 11  (幻读!)

MVCC(多版本并发控制)原理

每行记录有两个隐藏列:
- trx_id:最近修改该行的事务 ID
- roll_pointer:指向 undo log 中该行的上一个版本

undo log 版本链:
┌─────────────────────────────────────────────────────────┐
│  当前版本 (trx_id=300)                                   │
│  balance = 300                                           │
│  roll_pointer → ┌────────────────────────────────────┐  │
│                 │  版本2 (trx_id=200)                 │  │
│                 │  balance = 200                      │  │
│                 │  roll_pointer → ┌────────────────┐  │  │
│                 │                 │ 版本1 (trx_id=100) │  │
│                 │                 │ balance = 100  │  │  │
│                 │                 └────────────────┘  │  │
│                 └────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

ReadView(读视图):
- m_ids:创建 ReadView 时活跃的事务 ID 列表
- min_trx_id:活跃事务中最小的 ID
- max_trx_id:下一个要分配的事务 ID
- creator_trx_id:创建该 ReadView 的事务 ID

可见性判断:
1. trx_id < min_trx_id → 可见(事务已提交)
2. trx_id >= max_trx_id → 不可见(事务在 ReadView 之后开始)
3. min_trx_id <= trx_id < max_trx_id:
   - trx_id 在 m_ids 中 → 不可见(事务未提交)
   - trx_id 不在 m_ids 中 → 可见(事务已提交)

RR 和 RC 的区别

  • RC:每次 SELECT 都创建新的 ReadView
  • RR:只在第一次 SELECT 时创建 ReadView,后续复用

项目场景
项目中工资发放需要保证数据一致性:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public SalaryReport generateSalaryReport(int month, int year) {
    // 1. 查询工资数据(创建 ReadView)
    List<Salary> salaries = salaryMapper.selectByMonth(month, year);
    
    // 此时其他事务修改了工资数据,不影响这里的查询结果
    // 因为 RR 隔离级别下,ReadView 不变
    
    // 2. 汇总计算
    BigDecimal total = salaries.stream()
        .map(Salary::getAllSalary)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    
    return new SalaryReport(salaries, total);
}

Q24: 如何优化慢 SQL?

A:

慢 SQL 排查流程

1. 开启慢查询日志

-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

2. 使用 EXPLAIN 分析

EXPLAIN SELECT * FROM user WHERE username = 'wangwenjia';

EXPLAIN 关键字段

字段含义优化目标
type访问类型system > const > eq_ref > ref > range > index > ALL
key实际使用的索引不为 NULL
rows预估扫描行数越小越好
Extra额外信息Using index 最优

常见慢 SQL 及优化

-- ❌ 1. 索引失效:对索引列使用函数
SELECT * FROM user WHERE YEAR(create_time) = 2026;
-- ✅ 优化:
SELECT * FROM user WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01';

-- ❌ 2. 索引失效:隐式类型转换
SELECT * FROM user WHERE stu_id = 202424431029;  -- stu_id 是 varchar
-- ✅ 优化:
SELECT * FROM user WHERE stu_id = '202424431029';

-- ❌ 3. 索引失效:LIKE 以 % 开头
SELECT * FROM user WHERE username LIKE '%wenjia';
-- ✅ 优化:使用全文索引或搜索引擎

-- ❌ 4. 回表过多:SELECT *
SELECT * FROM user WHERE status = 1;
-- ✅ 优化:覆盖索引
SELECT id, username, name FROM user WHERE status = 1;
-- 建立索引:ALTER TABLE user ADD INDEX idx_status_username_name (status, username, name);

-- ❌ 5. 排序未使用索引
SELECT * FROM user ORDER BY create_time DESC LIMIT 10;
-- ✅ 优化:建立索引
ALTER TABLE user ADD INDEX idx_create_time (create_time);

项目场景
项目中查询值班记录的优化:

-- 原始 SQL(慢)
SELECT * FROM course_list_duty 
WHERE user_id = 100 
AND year = 2026 
AND month = 5
ORDER BY create_time DESC;

-- 优化1:建立联合索引
ALTER TABLE course_list_duty 
ADD INDEX idx_user_year_month (user_id, year, month);

-- 优化2:只查需要的字段
SELECT id, user_id, duty_type, duty_count 
FROM course_list_duty 
WHERE user_id = 100 AND year = 2026 AND month = 5;

7. Redis 缓存

Q25: 缓存穿透、缓存击穿、缓存雪崩的区别?如何解决?

A:

三种问题对比

问题触发条件后果解决方案
缓存穿透查询不存在的数据请求全部打到 DB布隆过滤器 / 缓存空值
缓存击穿热点 key 过期大量并发打到 DB互斥锁 / 逻辑过期
缓存雪崩大量 key 同时过期DB 压力骤增TTL 加随机值 / 多级缓存

1. 缓存穿透

场景:查询 id=-1 的用户(不存在)

请求 → Redis(没有)→ MySQL(没有)→ 返回空
恶意攻击:不断请求不存在的 id,全部打到 MySQL

解决方案:
1. 缓存空值:将空结果缓存,设置较短 TTL
2. 布隆过滤器:在 Redis 前加一层布隆过滤器,拦截不存在的 key
// 缓存空值方案
public User getUserById(Long id) {
    String key = "user:" + id;
    
    // 1. 查缓存
    Object cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        if (cached.equals("NULL")) {
            return null;  // 缓存的空值
        }
        return (User) cached;
    }
    
    // 2. 查数据库
    User user = userMapper.selectById(id);
    
    // 3. 写缓存
    if (user == null) {
        redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);  // 空值缓存 5 分钟
    } else {
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
    }
    
    return user;
}

2. 缓存击穿

场景:热点 key(如首页推荐)过期,大量请求同时打到 DB

解决方案:
1. 互斥锁:只让一个线程查 DB,其他线程等待
2. 逻辑过期:不设置 TTL,在 value 中记录过期时间
// 互斥锁方案
public User getHotUser(Long id) {
    String key = "user:hot:" + id;
    String lockKey = "lock:user:" + id;
    
    // 1. 查缓存
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) {
        return user;
    }
    
    // 2. 获取锁
    boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (!locked) {
        // 没获取到锁,等待后重试
        Thread.sleep(50);
        return getHotUser(id);  // 递归重试
    }
    
    try {
        // 3. 双重检查
        user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        
        // 4. 查数据库
        user = userMapper.selectById(id);
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        
        return user;
    } finally {
        redisTemplate.delete(lockKey);  // 释放锁
    }
}

3. 缓存雪崩

场景:大量 key 在同一时间过期,请求全部打到 DB

解决方案:
1. TTL 加随机值:避免同时过期
2. 多级缓存:本地缓存(Caffeine) + Redis
3. 限流降级:DB 压力大时返回默认值
// TTL 加随机值
public void setUserCache(User user) {
    String key = "user:" + user.getId();
    int baseTTL = 30;  // 基础 TTL 30 分钟
    int randomTTL = new Random().nextInt(10);  // 随机 0-10 分钟
    redisTemplate.opsForValue().set(key, user, baseTTL + randomTTL, TimeUnit.MINUTES);
}

项目场景
项目中用户信息缓存的设计:

@Service
public class UserService {
    
    // 本地缓存(一级缓存)
    private final Cache<Long, User> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();
    
    public User getUserById(Long id) {
        // 1. 查本地缓存
        User user = localCache.getIfPresent(id);
        if (user != null) {
            return user;
        }
        
        // 2. 查 Redis(二级缓存)
        String key = "user:" + id;
        user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            localCache.put(id, user);
            return user;
        }
        
        // 3. 查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            // 写入 Redis,TTL 加随机值
            int ttl = 30 + new Random().nextInt(10);
            redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.MINUTES);
            localCache.put(id, user);
        } else {
            // 缓存空值,防止穿透
            redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
        }
        
        return user;
    }
}

Q26: Redis 的数据结构有哪些?各有什么使用场景?

A:

五种基础数据结构

数据结构说明典型场景
String字符串/数字缓存、计数器、分布式锁
Hash哈希表对象存储(用户信息)
List双向链表消息队列、最新列表
Set无序集合去重、交集/并集/差集
Sorted Set有序集合排行榜、延迟队列

项目中的使用场景

// 1. String:验证码、在线状态
redisTemplate.opsForValue().set("captcha:" + uuid, "123456", 5, TimeUnit.MINUTES);
redisTemplate.opsForValue().set("online:" + userId, "1");

// 2. Hash:用户信息缓存
redisTemplate.opsForHash().put("user:info:" + userId, "name", "王文佳");
redisTemplate.opsForHash().put("user:info:" + userId, "phone", "13800138000");
Map<Object, Object> userInfo = redisTemplate.opsForHash().entries("user:info:" + userId);

// 3. List:消息队列
redisTemplate.opsForList().rightPush("message:queue", message);
Message msg = redisTemplate.opsForList().leftPop("message:queue");

// 4. Set:签到记录
redisTemplate.opsForSet().add("signin:2026-05-28", userId);
boolean signed = redisTemplate.opsForSet().isMember("signin:2026-05-28", userId);
Long signCount = redisTemplate.opsForSet().size("signin:2026-05-28");

// 5. Sorted Set:排行榜
redisTemplate.opsForZSet().add("ranking", "user1", 100);
redisTemplate.opsForZSet().add("ranking", "user2", 95);
Set<Object> topN = redisTemplate.opsForZSet().reverseRange("ranking", 0, 9);  // 前 10 名

项目场景
项目中登录失败次数的实现:

// 使用 String 的原子递增操作
public boolean checkLoginFailCount(String username) {
    String key = "login:fail:" + username;
    
    // 获取当前失败次数
    Integer count = (Integer) redisTemplate.opsForValue().get(key);
    
    if (count != null && count >= 5) {
        return false;  // 已锁定
    }
    
    return true;  // 未锁定
}

public void incrementLoginFailCount(String username) {
    String key = "login:fail:" + username;
    
    // 原子递增
    Long count = redisTemplate.opsForValue().increment(key);
    
    if (count == 1) {
        // 第一次失败,设置过期时间 30 分钟
        redisTemplate.expire(key, 30, TimeUnit.MINUTES);
    }
}

public void clearLoginFailCount(String username) {
    redisTemplate.delete("login:fail:" + username);
}

8. MyBatis 持久层

Q27: MyBatis 中 #{} 和 ${} 的区别?

A:

特性#{}${}
处理方式预编译参数替换字符串拼接
SQL 注入安全不安全
使用场景参数值表名、列名、ORDER BY
底层实现PreparedStatementStatement

#{} 的原理

// MyBatis 源码中的处理
// #{username} 会被替换为 ?
// 然后通过 PreparedStatement 设置参数值

// SQL: SELECT * FROM user WHERE username = #{username}
// 转换为: SELECT * FROM user WHERE username = ?
// 参数: ps.setString(1, "wangwenjia")

// 好处:
// 1. 防止 SQL 注入(参数会被转义)
// 2. 预编译,性能更好(相同 SQL 只编译一次)

${} 的使用场景

<!-- ❌ 错误:使用 ${} 拼接参数值(SQL 注入风险) -->
SELECT * FROM user WHERE username = '${username}'

<!-- ✅ 正确:使用 ${} 动态指定列名/表名 -->
SELECT * FROM ${tableName} WHERE id = #{id}

<!-- ✅ 正确:使用 ${} 动态排序 -->
SELECT * FROM user ORDER BY ${orderBy} ${orderDir}

<!-- ✅ 正确:使用 #{} 作为参数值 -->
SELECT * FROM user WHERE username = #{username}

SQL 注入示例

// 用户输入:username = "admin' OR '1'='1"

// 使用 #{}(安全)
// SQL: SELECT * FROM user WHERE username = ?
// 参数: "admin' OR '1'='1"  (整个字符串作为参数,不会注入)
// 结果: 查询不到用户

// 使用 ${}(危险)
// SQL: SELECT * FROM user WHERE username = 'admin' OR '1'='1'
// 结果: 返回所有用户!

项目场景
项目中动态查询用户列表:

<!-- UserMapper.xml -->
<select id="selectUserList" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
    <!-- 动态排序:列名必须用 ${},但只允许白名单中的值 -->
    ORDER BY 
    <choose>
        <when test="orderBy == 'create_time'">create_time</when>
        <when test="orderBy == 'username'">username</when>
        <otherwise>id</otherwise>
    </choose>
    ${orderDir}
</select>

Q28: MyBatis 的一级缓存和二级缓存?

A:

一级缓存(SqlSession 级别)

默认开启,同一个 SqlSession 内有效

SqlSession session = sqlSessionFactory.openSession();

// 第一次查询:查数据库,结果存入一级缓存
User user1 = session.selectOne("getUser", 1);

// 第二次相同查询:直接从一级缓存返回,不查数据库
User user2 = session.selectOne("getUser", 1);

// 执行增删改操作:清空一级缓存
session.update("updateUser", user);
User user3 = session.selectOne("getUser", 1);  // 重新查数据库

二级缓存(Mapper 级别)

需要手动开启,同一个 Mapper 共享

<!-- 开启二级缓存 -->
<mapper namespace="com.cc.mapper.UserMapper">
    <cache/>
    
    <!-- 或者详细配置 -->
    <cache
        eviction="FIFO"          <!-- 回收策略:FIFO/LRU/SOFT/WEAK -->
        flushInterval="60000"    <!-- 刷新间隔:60 秒 -->
        size="1024"              <!-- 缓存大小 -->
        readOnly="true"/>        <!-- 是否只读 -->
</mapper>

一级缓存 vs 二级缓存

特性一级缓存二级缓存
作用域SqlSessionMapper(namespace)
默认状态开启关闭
存储位置内存可配置(内存/Redis)
共享性不共享同一 Mapper 共享
失效时机增删改/commit/close增删改/commit

项目场景
项目中配置 MyBatis 二级缓存(使用 Redis):

<!-- pom.xml 引入 mybatis-redis-cache -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis-cache</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

<!-- UserMapper.xml -->
<mapper namespace="com.cc.mapper.UserMapper">
    <cache type="org.mybatis.caches.redis.RedisCache">
        <property name="host" value="localhost"/>
        <property name="port" value="6379"/>
        <property name="timeout" value="2000"/>
    </cache>
</mapper>

注意:项目中实际上没有使用二级缓存,因为:

  1. 多表关联查询时,二级缓存容易出现脏数据
  2. 使用 Redis 手动缓存更灵活可控
  3. Spring 管理的事务可能导致缓存不一致

9. 设计模式

Q29: 请介绍项目中用到的设计模式?

A:

项目中使用的设计模式

1. 策略模式(Strategy Pattern)

// 场景:排班算法可切换
public interface DutyScheduler {
    List<DutyAssignment> schedule(ScheduleContext context);
}

@Component("schedulerV1")
public class DutySchedulerV1 implements DutyScheduler {
    @Override
    public List<DutyAssignment> schedule(ScheduleContext context) {
        // V1 贪心算法
    }
}

@Component("schedulerV2")
public class DutySchedulerV2 implements DutyScheduler {
    @Override
    public List<DutyAssignment> schedule(ScheduleContext context) {
        // V2 五阶段流水线算法
    }
}

// 使用:通过配置切换算法
@Value("${cc.scheduler.version}")
private String schedulerVersion;

@Autowired
private ApplicationContext context;

public void schedule() {
    DutyScheduler scheduler = context.getBean("scheduler" + schedulerVersion, DutyScheduler.class);
    scheduler.schedule(context);
}

2. 模板方法模式(Template Method Pattern)

// 场景:五阶段流水线排班算法
public abstract class AbstractScheduler {
    
    // 模板方法:定义算法骨架
    public final ScheduleResult schedule(ScheduleContext context) {
        // Phase 1:数据预处理
        preprocess(context);
        
        // Phase 2:评分分配
        assign(context);
        
        // Phase 3:约束校验
        validate(context);
        
        // Phase 4:局部优化
        optimize(context);
        
        // Phase 5:结果生成
        return generateResult(context);
    }
    
    // 抽象方法:子类实现
    protected abstract void preprocess(ScheduleContext context);
    protected abstract void assign(ScheduleContext context);
    
    // 钩子方法:子类可选择性重写
    protected void optimize(ScheduleContext context) {
        // 默认不做优化
    }
}

3. 建造者模式(Builder Pattern)

// 场景:构建复杂对象
@Builder
@Data
public class OperationLog {
    private Long id;
    private Long userId;
    private String userName;
    private String operation;
    private String method;
    private String params;
    private String result;
    private Long duration;
    private String ip;
    private LocalDateTime createTime;
}

// 使用
OperationLog log = OperationLog.builder()
    .userId(userId)
    .userName(userName)
    .operation("新增用户")
    .method("UserController.addUser")
    .duration(100L)
    .ip(ip)
    .build();

4. 观察者模式(Observer Pattern)

// 场景:SSE/WebSocket 消息发布与订阅
public class SseConnectionManager {
    private final Map<String, SseEmitter> connections = new ConcurrentHashMap<>();
    
    // 订阅
    public void subscribe(String userId, SseEmitter emitter) {
        connections.put(userId, emitter);
    }
    
    // 发布:通知所有订阅者
    public void broadcast(String message) {
        connections.forEach((userId, emitter) -> {
            try {
                emitter.send(message);
            } catch (IOException e) {
                connections.remove(userId);
            }
        });
    }
    
    // 发布:通知指定订阅者
    public void notify(String userId, String message) {
        SseEmitter emitter = connections.get(userId);
        if (emitter != null) {
            try {
                emitter.send(message);
            } catch (IOException e) {
                connections.remove(userId);
            }
        }
    }
}

5. 代理模式(Proxy Pattern)

// 场景:Spring AOP 动态代理
@Aspect
@Component
public class LogAspect {
    
    @Around("execution(* com.cc.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        // 调用目标方法(通过代理对象)
        Object result = joinPoint.proceed();
        
        long duration = System.currentTimeMillis() - start;
        log.info("方法 {} 执行耗时 {}ms", joinPoint.getSignature().getName(), duration);
        
        return result;
    }
}

6. 单例模式(Singleton Pattern)

// 场景:Spring Bean 默认单例
@Service
public class UserService {
    // Spring 容器中只有一个实例
}

// 线程安全的单例实现
public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Q30: 单例模式有哪些实现方式?哪种最安全?

A:

五种实现方式

1. 饿汉式(线程安全)

public class Singleton {
    // 类加载时就创建实例
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
// 优点:简单,线程安全
// 缺点:类加载时就创建,可能浪费资源

2. 懒汉式(线程不安全)

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {  // ❌ 多线程下可能创建多个实例
            instance = new Singleton();
        }
        return instance;
    }
}

3. 懒汉式 + synchronized(线程安全,性能差)

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {  // 每次调用都加锁
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
// 缺点:性能差,每次获取实例都要获取锁

4. 双重检查锁(DCL)(推荐)

public class Singleton {
    private static volatile Singleton instance;  // 必须用 volatile
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {  // 第一次检查:避免不必要的同步
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查:防止重复创建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
// 优点:懒加载 + 线程安全 + 性能好
// 注意:必须用 volatile 禁止指令重排序

5. 静态内部类(推荐)

public class Singleton {
    private Singleton() {}
    
    // 静态内部类在第一次使用时才加载
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
// 优点:懒加载 + 线程安全 + 无锁 + 简单
// 原理:JVM 保证类加载的线程安全性

6. 枚举(最安全)

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务方法
    }
}

// 使用
Singleton.INSTANCE.doSomething();
// 优点:线程安全 + 防止反射攻击 + 防止反序列化创建新实例
// 缺点:不够灵活

项目场景
项目中使用 Spring 管理的单例 Bean:

@Service
public class DutySchedulerService {
    // Spring 默认是单例模式(singleton scope)
    // 容器中只有一个实例
    
    @Autowired
    private DutySchedulerV2 scheduler;
}

// 如果需要多例,可以指定 scope
@Service
@Scope("prototype")
public class PrototypeService {
    // 每次注入都会创建新实例
}

10. 分布式基础

Q31: 分布式 ID 生成方案有哪些?

A:

方案优点缺点适用场景
UUID简单、无网络开销无序、太长、不适合做主键Session ID
数据库自增简单、有序单点瓶颈、分库分表困难小规模系统
雪花算法有序、高性能、趋势递增时钟回拨问题分布式系统
Redis INCR简单、有序依赖 Redis中等规模
Leaf/Tinyid美团/百度开源方案需要额外部署大规模系统

雪花算法(Snowflake)详解

64 位 ID 结构:

┌─┬─────────────────────────────────────┬───────────┬───────────┐
│1│          41 位时间戳                  │ 10 位机器ID│ 12 位序列号│
└─┴─────────────────────────────────────┴───────────┴───────────┘
  │                                      │           │
  │                                      │           └─ 同一毫秒可生成 4096 个 ID
  │                                      └─ 支持 1024 个节点
  └─ 符号位(始终为 0)

时间戳精度:毫秒级
可用时间:约 69 年
每秒可生成:4096 * 1000 = 409.6 万个 ID

项目中的实现

public class SnowflakeIdUtil {
    private final long workerId;      // 机器 ID(0-1023)
    private final long datacenterId;  // 数据中心 ID(0-31)
    private long sequence = 0L;       // 序列号
    private long lastTimestamp = -1L; // 上次时间戳
    
    // 起始时间戳(2024-01-01 00:00:00)
    private final long epoch = 1704067200000L;
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        // 时钟回拨检查
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成 ID");
        }
        
        if (timestamp == lastTimestamp) {
            // 同一毫秒,序列号递增
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                // 序列号溢出,等待下一毫秒
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        // 组装 ID
        return ((timestamp - epoch) << 22)
             | (datacenterId << 17)
             | (workerId << 12)
             | sequence;
    }
}

项目场景
项目中使用雪花算法生成操作日志 ID:

@Component
public class OperationLogService {
    
    @Autowired
    private SnowflakeIdUtil snowflakeIdUtil;
    
    public void saveLog(OperationLog log) {
        // 使用雪花算法生成唯一 ID
        log.setId(snowflakeIdUtil.nextId());
        operationLogMapper.insert(log);
    }
}

Q32: 如何保证接口的幂等性?

A:

幂等性:同一个请求执行多次与执行一次效果相同。

为什么需要幂等?

  • 网络超时重试
  • 用户重复点击
  • 消息队列重复消费
  • 分布式系统中的重试机制

五种实现方案

1. Token 机制

// 1. 服务端生成 Token,存入 Redis
@GetMapping("/getToken")
public String getToken() {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set("token:" + token, "1", 10, TimeUnit.MINUTES);
    return token;
}

// 2. 前端提交时携带 Token
@PostMapping("/submit")
public Result submit(@RequestHeader String token) {
    // 原子性删除并检查 Token
    Boolean deleted = redisTemplate.delete("token:" + token);
    if (!deleted) {
        return Result.error("请勿重复提交");
    }
    // 执行业务逻辑...
    return Result.success();
}

2. 数据库唯一索引

// 场景:防止重复下单
// 建立唯一索引
// ALTER TABLE `order` ADD UNIQUE KEY uk_order_no (order_no);

@PostMapping("/createOrder")
public Result createOrder(@RequestBody OrderDTO dto) {
    try {
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        orderMapper.insert(order);
        return Result.success();
    } catch (DuplicateKeyException e) {
        return Result.error("订单已存在");
    }
}

3. 乐观锁

// 场景:防止重复支付
// UPDATE `order` SET status = 'paid', version = version + 1 
// WHERE id = #{id} AND version = #{version}

@Update("UPDATE `order` SET status = 'paid', version = version + 1 " +
        "WHERE id = #{id} AND version = #{version}")
int payOrder(@Param("id") Long id, @Param("version") Integer version);

public Result payOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    int rows = orderMapper.payOrder(orderId, order.getVersion());
    if (rows == 0) {
        return Result.error("请勿重复支付");
    }
    return Result.success();
}

4. 状态机

// 场景:订单状态只能单向流转
// 待支付 → 已支付 → 已发货 → 已完成

public Result payOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    
    // 只有待支付状态才能支付
    if (!"pending".equals(order.getStatus())) {
        return Result.error("订单状态不正确,无法支付");
    }
    
    order.setStatus("paid");
    orderMapper.updateById(order);
    return Result.success();
}

5. 分布式锁

// 场景:防止重复处理
public Result processOrder(Long orderId) {
    String lockKey = "lock:order:" + orderId;
    
    // 获取分布式锁
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (!locked) {
        return Result.error("订单正在处理中");
    }
    
    try {
        // 执行业务逻辑
        return doProcessOrder(orderId);
    } finally {
        redisTemplate.delete(lockKey);
    }
}

项目场景
项目中防止重复签到:

public Result signIn(SignInDTO dto) {
    String key = "signin:" + LocalDate.now() + ":" + dto.getUserId();
    
    // 使用 SETNX 保证幂等
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(key, "1", 24, TimeUnit.HOURS);
    
    if (!success) {
        return Result.error("今日已签到,请勿重复签到");
    }
    
    // 保存签到记录
    signInMapper.insert(dto);
    return Result.success("签到成功");
}

11. 场景题 & 综合题

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

A:

秒杀系统的核心挑战:高并发 + 超卖 + 数据一致性

架构设计

┌─────────────────────────────────────────────────────────┐
│                    秒杀系统架构                           │
│                                                          │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐         │
│   │  前端     │    │  网关     │    │  服务层   │         │
│   │ 按钮防抖  │ →  │  限流     │ →  │  Redis   │         │
│   │ 倒计时   │    │  去重     │    │  预扣减   │         │
│   └──────────┘    └──────────┘    └────┬─────┘         │
│                                        │                │
│                                        ↓                │
│                                  ┌──────────┐          │
│                                  │  消息队列  │          │
│                                  │  异步下单  │          │
│                                  └────┬─────┘          │
│                                       │                │
│                                       ↓                │
│                                  ┌──────────┐          │
│                                  │  数据库    │          │
│                                  │  乐观锁   │          │
│                                  └──────────┘          │
└─────────────────────────────────────────────────────────┘

核心代码

// 1. Redis 预扣减库存
public boolean deductStock(Long itemId, Integer count) {
    String key = "stock:" + itemId;
    
    // Lua 脚本保证原子性
    String script = """
        local stock = redis.call('get', KEYS[1])
        if stock == false then
            return -1
        end
        stock = tonumber(stock)
        if stock < tonumber(ARGV[1]) then
            return 0
        end
        redis.call('decrby', KEYS[1], ARGV[1])
        return 1
        """;
    
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        List.of(key),
        String.valueOf(count)
    );
    
    return result == 1;
}

// 2. 异步下单(消息队列)
@PostMapping("/seckill")
public Result seckill(@RequestBody SeckillRequest request) {
    // 1. 用户维度去重(防止重复点击)
    String userKey = "seckill:user:" + request.getUserId() + ":" + request.getItemId();
    Boolean success = redisTemplate.opsForValue().setIfAbsent(userKey, "1", 1, TimeUnit.HOURS);
    if (!success) {
        return Result.error("请勿重复下单");
    }
    
    // 2. Redis 预扣减库存
    boolean deducted = deductStock(request.getItemId(), 1);
    if (!deducted) {
        return Result.error("库存不足");
    }
    
    // 3. 发送 MQ 消息,异步创建订单
    rabbitTemplate.convertAndSubmit("seckill.order", request);
    
    return Result.success("下单成功,请稍后查看订单");
}

// 3. 数据库乐观锁
@Update("UPDATE stock SET count = count - 1, version = version + 1 " +
        "WHERE id = #{id} AND count > 0 AND version = #{version}")
int deductStock(@Param("id") Long id, @Param("version") Integer version);

Q34: 线上服务 CPU 飙到 100%,如何排查?

A:

排查步骤

# 1. 找到 CPU 占用最高的 Java 进程
top
# 记录 PID,如 12345

# 2. 找到 CPU 占用最高的线程
top -Hp 12345
# 记录线程 TID,如 12367

# 3. 线程 ID 转十六进制
printf "%x\n" 12367
# 输出:304f

# 4. 抓取线程栈
jstack 12345 | grep 304f -A 30

# 5. 分析线程栈
# 查看是哪个方法在消耗 CPU

常见原因及解决

// 原因1:死循环
while (true) {
    // 没有退出条件
    doSomething();
}

// 原因2:正则表达式回溯(ReDoS)
String regex = "(a+)+b";  // 灾难性回溯
input.matches(regex);     // 输入 "aaaaaaaaaaaaac" 会导致 CPU 100%

// 原因3:频繁 Full GC
// 使用 jstat 查看 GC 情况
jstat -gcutil 12345 1000

// 原因4:死锁
jstack 12345 | grep -i "deadlock"

项目场景
项目中配置 JVM 监控:

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: cc-server
# 使用 arthas 在线诊断
java -jar arthas-boot.jar

# 查看最繁忙的线程
thread -n 3

# 查看方法耗时
trace com.cc.service.UserService getUserById

# 查看堆内存使用情况
memory

Q35: 如何设计一个高可用的系统?

A:

高可用的核心指标:SLA(服务级别协议)

SLA年宕机时间说明
99%3.65 天两个 9
99.9%8.76 小时三个 9
99.99%52.6 分钟四个 9
99.999%5.26 分钟五个 9

高可用架构设计

┌─────────────────────────────────────────────────────────┐
│                    高可用系统架构                          │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  接入层                                           │   │
│   │  CDN + DNS 负载均衡                               │   │
│   └─────────────────────────────────────────────────┘   │
│                        ↓                                │
│   ┌─────────────────────────────────────────────────┐   │
│   │  网关层                                           │   │
│   │  Nginx / Spring Cloud Gateway                     │   │
│   │  限流 + 熔断 + 降级                                │   │
│   └─────────────────────────────────────────────────┘   │
│                        ↓                                │
│   ┌─────────────────────────────────────────────────┐   │
│   │  服务层                                           │   │
│   │  多实例部署 + 负载均衡                             │   │
│   │  服务注册与发现(Nacos/Eureka)                     │   │
│   └─────────────────────────────────────────────────┘   │
│                        ↓                                │
│   ┌─────────────────────────────────────────────────┐   │
│   │  数据层                                           │   │
│   │  MySQL 主从 + 读写分离                             │   │
│   │  Redis Sentinel / Cluster                         │   │
│   │  分布式存储(MinIO)                               │   │
│   └─────────────────────────────────────────────────┘   │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  可观测性                                         │   │
│   │  日志(ELK)+ 监控(Prometheus)+ 链路追踪(SkyWalking)│
│   └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

核心手段

1. 冗余设计:无单点故障

# 多实例部署
spring:
  cloud:
    nacos:
      discovery:
        # 注册多个实例

2. 故障隔离

// 熔断器(Sentinel)
@SentinelResource(value = "getUser", fallback = "getUserFallback")
public User getUser(Long id) {
    return userMapper.selectById(id);
}

public User getUserFallback(Long id, Throwable e) {
    return new User();  // 返回默认值
}

3. 限流降级

// 限流(Guava RateLimiter)
RateLimiter rateLimiter = RateLimiter.create(100);  // 每秒 100 个请求

@GetMapping("/api/user")
public Result getUser() {
    if (!rateLimiter.tryAcquire()) {
        return Result.error("系统繁忙,请稍后重试");
    }
    return Result.success(userService.getUser());
}

4. 数据备份

# MySQL 全量备份
mysqldump -u root -p cc_database > backup_$(date +%Y%m%d).sql

# MySQL 增量备份(binlog)
mysqlbinlog --start-datetime="2026-05-28 00:00:00" mysql-bin.000001 > incremental.sql

项目场景
项目的高可用设计:

// 1. Python 服务降级策略
@Service
public class SalaryService {
    
    @Retryable(value = RestClientException.class, maxAttempts = 3)
    public SalaryPrediction predict(Long userId) {
        return pythonClient.predict(userId);
    }
    
    @Recover
    public SalaryPrediction recover(RestClientException e, Long userId) {
        // 降级:使用本地公式计算
        return localCalculator.calculate(userId);
    }
}

// 2. Redis 连接池配置
spring:
  redis:
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
    sentinel:
      master: mymaster
      nodes: 192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379

12. Java 进阶(新增)

Q36: Java 泛型的类型擦除是什么?有什么限制?

A:

类型擦除:Java 泛型在编译期检查类型安全,编译后会擦除泛型信息,运行时不保留。

// 编译前
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

// 编译后(类型擦除)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);  // 强制类型转换

泛型的限制

// ❌ 1. 不能使用基本类型
// List<int> list = new ArrayList<>();  // 编译错误
List<Integer> list = new ArrayList<>();  // ✅ 使用包装类

// ❌ 2. 不能实例化泛型类型
// public <T> void method() {
//     T obj = new T();  // 编译错误
// }

// ❌ 3. 不能创建泛型数组
// List<String>[] arr = new List<String>[10];  // 编译错误

// ❌ 4. 不能用 instanceof 判断泛型类型
// if (list instanceof List<String>) {}  // 编译错误
if (list instanceof List<?>) {}  // ✅ 使用通配符

项目场景
项目中统一响应封装使用泛型:

@Data
public class Result<T> {
    private int code;
    private String msg;
    private T data;
    
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("success");
        result.setData(data);
        return result;
    }
}

// 使用
Result<User> result = Result.success(user);
Result<List<User>> result = Result.success(userList);

Q37: Java 反射的原理?性能影响?

A:

反射原理:通过 Class 对象在运行时获取类的结构信息,并动态调用方法、访问字段。

// 获取 Class 对象的三种方式
Class<?> clazz1 = Class.forName("com.cc.entity.User");
Class<?> clazz2 = User.class;
Class<?> clazz3 = new User().getClass();

// 创建实例
User user = (User) clazz1.getDeclaredConstructor().newInstance();

// 获取并调用方法
Method method = clazz1.getMethod("setName", String.class);
method.invoke(user, "王文佳");

// 获取并设置字段(包括私有)
Field field = clazz1.getDeclaredField("name");
field.setAccessible(true);  // 突破 private 限制
field.set(user, "王文佳");

性能影响
| 操作 | 耗时(相对值) |
|------|--------------|
| 直接调用方法 | 1x |
| 反射调用方法 | 5-10x |
| 反射 + setAccessible | 3-5x |

优化方案

  1. 缓存 Method/Field 对象,避免重复获取
  2. 使用 MethodHandle(JDK 7+)替代反射
  3. 使用字节码生成(CGLib、Javassist)

项目场景
项目中 AOP 自动填充切面使用反射:

@Aspect
@Component
public class AutoFillAspect {
    
    @Before("@annotation(autoFill)")
    public void autoFill(JoinPoint joinPoint, AutoFill autoFill) {
        Object entity = joinPoint.getArgs()[0];
        Class<?> clazz = entity.getClass();
        
        // 使用反射设置公共字段
        if (autoFill.operationType() == OperationType.INSERT) {
            Method setCreateTime = clazz.getMethod("setCreateTime", LocalDateTime.class);
            Method setUpdateTime = clazz.getMethod("setUpdateTime", LocalDateTime.class);
            
            setCreateTime.invoke(entity, LocalDateTime.now());
            setUpdateTime.invoke(entity, LocalDateTime.now());
        }
    }
}

Q38: Java 注解的原理?如何自定义注解?

A:

注解本质:注解是一个实现了 java.lang.annotation.Annotation 接口的特殊接口,运行时通过反射获取。

// 自定义注解
@Target(ElementType.METHOD)      // 可以用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
public @interface OperationLog {
    String value() default "";    // 注解属性
    String module() default "";
}

// 使用
@PostMapping("/admin/user")
@OperationLog(value = "新增用户", module = "用户管理")
public Result addUser(@RequestBody UserDTO dto) {
    userService.addUser(dto);
    return Result.success();
}

元注解说明
| 元注解 | 作用 |
|--------|------|
| @Target | 注解可以用在哪里(METHOD、FIELD、TYPE 等) |
| @Retention | 注解保留时机(SOURCE、CLASS、RUNTIME) |
| @Documented | 是否包含在 Javadoc 中 |
| @Inherited | 子类是否继承父类的注解 |

项目场景
项目中自定义 @OperationLog 注解配合 AOP 使用:

// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    String value();
}

// 2. AOP 切面处理
@Aspect
@Component
public class OperationLogAspect {
    
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
        String operation = operationLog.value();  // 获取注解属性
        
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - start;
        
        // 保存操作日志
        saveLog(operation, duration);
        
        return result;
    }
}

Q39: Java Stream API 常用操作?parallelStream 线程安全吗?

A:

Stream 常用操作

List<User> users = userMapper.selectAll();

// 1. filter:过滤
List<User> activeUsers = users.stream()
    .filter(u -> u.getStatus() == 1)
    .collect(Collectors.toList());

// 2. map:转换
List<String> names = users.stream()
    .map(User::getName)
    .collect(Collectors.toList());

// 3. sorted:排序
List<User> sorted = users.stream()
    .sorted(Comparator.comparing(User::getCreateTime).reversed())
    .collect(Collectors.toList());

// 4. groupBy:分组
Map<Integer, List<User>> groupByGrade = users.stream()
    .collect(Collectors.groupingBy(User::getGrade));

// 5. reduce:聚合
int totalScore = users.stream()
    .map(User::getScore)
    .reduce(0, Integer::sum);

// 6. Collectors 工具
String nameStr = users.stream()
    .map(User::getName)
    .collect(Collectors.joining(", "));

parallelStream 线程安全吗?

// ❌ 不安全:多线程修改同一个 List
List<User> result = new ArrayList<>();
users.parallelStream()
    .filter(u -> u.getScore() > 80)
    .forEach(result::add);  // ArrayList 不是线程安全的!

// ✅ 安全方案1:使用线程安全的集合
List<User> result = Collections.synchronizedList(new ArrayList<>());

// ✅ 安全方案2:使用 collect 而不是 forEach
List<User> result = users.parallelStream()
    .filter(u -> u.getScore() > 80)
    .collect(Collectors.toList());  // collect 内部处理了线程安全

项目场景
项目中批量计算工资时使用 Stream:

public SalaryReport generateReport(List<Salary> salaries) {
    // 按用户分组,计算每个用户的总工资
    Map<Long, BigDecimal> userSalaryMap = salaries.stream()
        .collect(Collectors.groupingBy(
            Salary::getUserId,
            Collectors.reducing(BigDecimal.ZERO, Salary::getAllSalary, BigDecimal::add)
        ));
    
    // 计算平均工资
    BigDecimal avgSalary = userSalaryMap.values().stream()
        .reduce(BigDecimal.ZERO, BigDecimal::add)
        .divide(BigDecimal.valueOf(userSalaryMap.size()), 2, RoundingMode.HALF_UP);
    
    return new SalaryReport(userSalaryMap, avgSalary);
}

Q40: CompletableFuture 如何实现异步编排?

A:

CompletableFuture 核心方法

// 1. 创建异步任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return queryFromDB();
});

// 2. 链式调用
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> queryUser(userId))           // 异步查询用户
    .thenApply(user -> queryOrders(user.getId()))   // 同步查询订单
    .thenApply(orders -> formatResult(orders))      // 同步格式化
    .exceptionally(e -> "error: " + e.getMessage()); // 异常处理

// 3. 组合多个异步任务
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> queryUser(id));
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> queryOrders(id));

// 等待两个任务都完成
CompletableFuture.allOf(userFuture, orderFuture).join();

// 4. 多任务并行,任意一个完成
CompletableFuture<Object> anyResult = CompletableFuture.anyOf(
    queryFromDB(),
    queryFromRedis(),
    queryFromLocal()
);

常用 API 速查

方法说明
supplyAsync异步执行,有返回值
runAsync异步执行,无返回值
thenApply同步转换结果
thenApplyAsync异步转换结果
thenCompose扁平化嵌套 Future
thenCombine合并两个 Future 的结果
allOf等待所有 Future 完成
anyOf任意一个 Future 完成
exceptionally异常处理
handle统一处理结果和异常

项目场景
项目中并行查询多个数据源:

public DashboardData getDashboardData(Long userId) {
    // 并行查询
    CompletableFuture<User> userFuture = CompletableFuture
        .supplyAsync(() -> userService.getUser(userId));
    
    CompletableFuture<List<DutyRecord>> dutyFuture = CompletableFuture
        .supplyAsync(() -> dutyService.getRecords(userId));
    
    CompletableFuture<Salary> salaryFuture = CompletableFuture
        .supplyAsync(() -> salaryService.getLatest(userId));
    
    // 等待所有任务完成
    CompletableFuture.allOf(userFuture, dutyFuture, salaryFuture).join();
    
    // 组装结果
    return DashboardData.builder()
        .user(userFuture.join())
        .dutyRecords(dutyFuture.join())
        .salary(salaryFuture.join())
        .build();
}

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

A:

循环依赖场景

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

Spring 的三级缓存

一级缓存(singletonObjects):完整的 Bean 对象
二级缓存(earlySingletonObjects):早期暴露的 Bean(半成品)
三级缓存(singletonFactories):Bean 工厂(用于创建代理对象)

创建 A 的过程:
1. 实例化 A(调用构造方法)
2. 将 A 的工厂放入三级缓存
3. 填充属性,发现依赖 B
4. 去缓存找 B,找不到
5. 开始创建 B
6. 实例化 B
7. 填充属性,发现依赖 A
8. 去缓存找 A,在三级缓存找到 A 的工厂
9. 通过工厂获取 A 的早期引用(可能是代理对象)
10. 将 A 的早期引用放入二级缓存,从三级缓存删除
11. B 完成初始化,放入一级缓存
12. A 继续填充属性,获取到完整的 B
13. A 完成初始化,放入一级缓存

为什么需要三级缓存?

  • 如果不需要 AOP,两级缓存就够了
  • 三级缓存的 ObjectFactory 可以在需要时创建代理对象
  • 避免在没有循环依赖时过早创建代理

哪些情况无法解决?

// ❌ 构造器注入的循环依赖无法解决
@Service
public class A {
    public A(B b) {}  // 构造时就需要 B
}

@Service
public class B {
    public B(A a) {}  // 构造时就需要 A
}

// ❌ @Scope("prototype") 的循环依赖无法解决
@Service
@Scope("prototype")
public class A {
    @Autowired
    private B b;
}

项目场景
项目中排班算法的循环依赖解决方案:

// 方案1:使用 @Lazy 延迟注入
@Service
public class CourseServiceImpl {
    @Lazy
    @Autowired
    private IntentionDutyMapper intentionDutyMapper;
}

// 方案2:提取公共逻辑到第三个类(推荐)
@Service
public class ScheduleDataHelper {
    @Autowired
    private CourseMapper courseMapper;
    
    @Autowired
    private IntentionDutyMapper intentionDutyMapper;
    
    public ScheduleData prepareData() {
        // 公共的数据查询逻辑
    }
}

Q42: Spring Bean 的作用域有哪些?

A:

作用域说明使用场景
singleton默认,容器中只有一个实例无状态的 Service、Mapper
prototype每次获取都创建新实例有状态的对象
request每个 HTTP 请求创建一个实例Web 应用
session每个 HTTP Session 创建一个实例Web 应用
application每个 ServletContext 创建一个实例Web 应用
// 指定作用域
@Service
@Scope("prototype")
public class PrototypeService {
    private int count = 0;  // 有状态
    
    public int increment() {
        return ++count;
    }
}

// 使用
@Autowired
private PrototypeService service1;  // 每次注入都是新实例

singleton Bean 注入 prototype Bean 的问题

@Service  // singleton
public class SingletonService {
    @Autowired
    private PrototypeService prototypeService;  // 只会注入一次!
    
    public void method() {
        // 每次调用都是同一个 prototypeService 实例
        prototypeService.increment();
    }
}

// 解决方案1:使用 @Lookup
@Service
public abstract class SingletonService {
    @Lookup
    public abstract PrototypeService getPrototypeService();
}

// 解决方案2:注入 ObjectFactory
@Service
public class SingletonService {
    @Autowired
    private ObjectProvider<PrototypeService> prototypeServiceProvider;
    
    public void method() {
        PrototypeService ps = prototypeServiceProvider.getObject();  // 每次获取新实例
    }
}

Q43: MySQL 中有哪些锁?行锁、表锁、间隙锁的区别?

A:

按粒度分类

锁类型粒度并发度开销
表锁整张表
行锁单行
间隙锁索引间隙--

InnoDB 行锁的三种算法

1. Record Lock:锁定单条记录
2. Gap Lock:锁定索引记录之间的间隙(不包括记录本身)
3. Next-Key Lock:Record Lock + Gap Lock(左开右闭区间)

示例:表中有 id = 1, 5, 10 的记录

SELECT * FROM user WHERE id = 5 FOR UPDATE;
→ Record Lock:锁定 id=5 的记录

SELECT * FROM user WHERE id > 5 AND id < 10 FOR UPDATE;
→ Gap Lock:锁定 (5, 10) 的间隙

SELECT * FROM user WHERE id >= 5 FOR UPDATE;
→ Next-Key Lock:锁定 [5, +∞)

行锁的加锁规则

-- 1. 等值查询唯一索引,命中 → Record Lock
SELECT * FROM user WHERE id = 5 FOR UPDATE;

-- 2. 等值查询唯一索引,未命中 → Gap Lock
SELECT * FROM user WHERE id = 6 FOR UPDATE;  -- id=6 不存在

-- 3. 等值查询非唯一索引 → Next-Key Lock 退化为 Gap Lock
SELECT * FROM user WHERE age = 20 FOR UPDATE;  -- age 有索引但不唯一

-- 4. 范围查询 → Next-Key Lock
SELECT * FROM user WHERE age > 20 FOR UPDATE;

项目场景
项目中防止并发修改工资数据:

@Transactional
public void updateSalary(Long userId, BigDecimal newSalary) {
    // SELECT ... FOR UPDATE 加行锁,防止并发修改
    Salary salary = salaryMapper.selectForUpdate(userId);
    
    if (salary == null) {
        throw new BusinessException(404, "工资记录不存在");
    }
    
    salary.setAllSalary(newSalary);
    salaryMapper.updateById(salary);
}
<!-- SalaryMapper.xml -->
<select id="selectForUpdate" resultType="Salary">
    SELECT * FROM salary WHERE user_id = #{userId} FOR UPDATE
</select>

Q44: Redis 的持久化机制?RDB 和 AOF 的区别?

A:

特性RDBAOF
原理定时生成内存快照追加写命令日志
触发方式save/bgsave/自动always/everysec/no
恢复速度快(二进制文件)慢(重放命令)
数据安全可能丢失最后一次快照后的数据最多丢失 1 秒数据
文件大小小(压缩)大(需 AOF 重写压缩)

RDB 配置

# redis.conf
save 900 1      # 900 秒内至少 1 个 key 变化,触发 bgsave
save 300 10     # 300 秒内至少 10 个 key 变化
save 60 10000   # 60 秒内至少 10000 个 key 变化

dbfilename dump.rdb
dir /var/lib/redis

AOF 配置

# redis.conf
appendonly yes
appendfilename "appendonly.aof"

# 同步策略
appendfsync always    # 每次写命令都同步(最安全,最慢)
appendfsync everysec  # 每秒同步一次(推荐)
appendfsync no        # 由操作系统决定(最快,最不安全)

# AOF 重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

Redis 4.0+ 混合持久化

AOF 重写时,将 RDB 格式写入 AOF 头部,后续增量命令追加到尾部

┌─────────────────────────────────┐
│  RDB 格式(全量数据)             │
├─────────────────────────────────┤
│  AOF 格式(增量命令)             │
└─────────────────────────────────┘

优点:兼顾恢复速度和数据安全

项目场景
项目中 Redis 配置:

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

Q45: Redis 集群方案有哪些?主从、哨兵、Cluster 的区别?

A:

方案架构特点适用场景
主从复制1 主 N 从读写分离,手动故障转移读多写少
哨兵(Sentinel)1 主 N 从 + 哨兵自动故障转移,监控中小规模
Cluster多主多从数据分片,高可用大规模

主从复制

Master(写)──→ Slave1(读)
     │
     └──→ Slave2(读)

配置:
# slave.conf
replicaof 192.168.1.10 6379
masterauth your_password

哨兵模式

         ┌─── Sentinel1
Master ──┼─── Sentinel2
  ↓      └─── Sentinel3
Slave

哨兵职责:
1. 监控:检测主从节点是否正常
2. 通知:主节点故障时通知客户端
3. 自动故障转移:选举新主节点

Cluster 模式

┌──────────┐  ┌──────────┐  ┌──────────┐
│ Master1  │  │ Master2  │  │ Master3  │
│ 0-5460   │  │ 5461-10922│ │ 10923-16383│
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │             │
┌────┴─────┐  ┌────┴─────┐  ┌────┴─────┐
│ Slave1   │  │ Slave2   │  │ Slave3   │
└──────────┘  └──────────┘  └──────────┘

特点:
- 16384 个哈希槽(slot),数据自动分片
- 每个 Master 负责一部分 slot
- 支持自动故障转移
- 不支持跨 slot 的事务和 Lua 脚本

项目场景
项目中 Redis 哨兵配置:

spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.1.10:26379
        - 192.168.1.11:26379
        - 192.168.1.12:26379
    password: your_password

Q46: 消息队列的使用场景?如何保证消息不丢失?

A:

消息队列的使用场景

场景说明示例
异步处理耗时操作异步执行发送邮件、短信
流量削峰高峰期缓冲请求秒杀下单
系统解耦降低系统间耦合度订单→库存→支付
日志收集统一收集日志ELK 日志系统

消息丢失的三个环节

生产者 ──→ MQ Broker ──→ 消费者

1. 生产者丢失:消息发送到 Broker 前丢失
2. Broker 丢失:消息在 Broker 存储后丢失
3. 消费者丢失:消息消费后但处理失败

保证消息不丢失的方案

// 1. 生产者确认机制(RabbitMQ)
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
    if (!ack) {
        log.error("消息发送失败: {}", cause);
        // 重试或记录到数据库
    }
});

// 2. 消息持久化
rabbitTemplate.convertAndSend(exchange, routingKey, message, msg -> {
    msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
    return msg;
});

// 3. 消费者手动确认
@RabbitListener(queues = "order.queue")
public void handleMessage(Message message, Channel channel) throws IOException {
    try {
        // 处理消息
        processMessage(message);
        // 手动确认
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 拒绝消息,重新入队
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
    }
}

项目场景
项目中虽然没有使用消息队列,但操作日志的异步写入体现了类似思想:

// 使用线程池异步写入日志(类似消息队列的异步处理)
@Async("operationLogExecutor")
public void saveLog(OperationLog log) {
    operationLogMapper.insert(log);
}

Q47: 如何设计一个 JWT 认证系统?

A:

JWT 结构

Header.Payload.Signature

Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"userId": 100, "username": "wangwenjia", "exp": 1717000000}
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

完整认证流程

┌──────────┐                    ┌──────────┐
│  客户端   │                    │  服务端   │
└────┬─────┘                    └────┬─────┘
     │                               │
     │  1. 登录请求(用户名+密码)     │
     │ ────────────────────────────→ │
     │                               │  2. 验证密码
     │                               │  3. 生成 JWT
     │  4. 返回 JWT                  │
     │ ←──────────────────────────── │
     │                               │
     │  5. 请求 API(Header: Bearer JWT)
     │ ────────────────────────────→ │
     │                               │  6. 验证 JWT
     │                               │  7. 提取 userId
     │  8. 返回数据                  │
     │ ←──────────────────────────── │

项目中的实现

// 1. JWT 工具类
public class JwtUtil {
    private static final String SECRET = "cc-secret-key";
    
    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
            .setClaims(claims)
            .setExpiration(new Date(System.currentTimeMillis() + 30 * 24 * 3600 * 1000))
            .signWith(SignatureAlgorithm.HS256, SECRET)
            .compact();
    }
    
    public static Claims parseToken(String token) {
        return Jwts.parser()
            .setSigningKey(SECRET)
            .parseClaimsJws(token)
            .getBody();
    }
}

// 2. 登录接口
@PostMapping("/user/common/login")
public Result login(@RequestBody LoginDTO dto) {
    User user = userMapper.selectByUsername(dto.getUsername());
    if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) {
        return Result.error("用户名或密码错误");
    }
    
    // 生成 JWT
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", user.getId());
    claims.put("username", user.getUsername());
    String token = JwtUtil.generateToken(claims);
    
    // 存入 Redis(在线状态)
    redisTemplate.opsForValue().set("online:" + user.getId(), "1", 30, TimeUnit.DAYS);
    
    return Result.success(Map.of("token", token, "user", user));
}

// 3. JWT 拦截器
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token");
        if (token == null) {
            response.setStatus(401);
            return false;
        }
        
        try {
            Claims claims = JwtUtil.parseToken(token);
            Long userId = claims.get("userId", Long.class);
            
            // 检查在线状态
            if (!Boolean.TRUE.equals(redisTemplate.hasKey("online:" + userId))) {
                response.setStatus(401);
                return false;
            }
            
            // 存入 ThreadLocal
            BaseContext.setCurrentId(userId);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        BaseContext.clear();  // 清理 ThreadLocal
    }
}

Q48: SSE 和 WebSocket 的区别?如何实现?

A:

特性SSEWebSocket
通信方向单向(服务端→客户端)双向
协议HTTP独立协议(ws://)
自动重连浏览器内置需手动实现
数据格式文本文本/二进制
适用场景通知推送、AI 流式输出聊天、游戏、协同编辑

SSE 实现

// 服务端
@GetMapping("/user/sse/connect/{userId}")
public SseEmitter connect(@PathVariable Long userId) {
    SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);  // 30 分钟超时
    
    // 注册回调
    emitter.onCompletion(() -> connectionManager.removeConnection(userId));
    emitter.onTimeout(() -> connectionManager.removeConnection(userId));
    emitter.onError(e -> connectionManager.removeConnection(userId));
    
    // 保存连接
    connectionManager.addConnection(userId, emitter);
    
    return emitter;
}

// 发送消息
public void sendMessage(Long userId, String message) {
    SseEmitter emitter = connectionManager.get(userId);
    if (emitter != null) {
        emitter.send(SseEmitter.event()
            .id(UUID.randomUUID().toString())
            .data(message));
    }
}
// 客户端
const eventSource = new EventSource('/user/sse/connect/100');

eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('收到消息:', data);
};

eventSource.onerror = (event) => {
    console.log('连接断开,自动重连...');
};

WebSocket 实现

// 服务端
@Component
public class WebSocketHandler extends TextWebSocketHandler {
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String userId = getUserId(session);
        sessions.put(userId, session);
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        // 处理客户端消息
        String payload = message.getPayload();
        // 业务逻辑...
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String userId = getUserId(session);
        sessions.remove(userId);
    }
}

项目场景
项目中使用 SSE 实现 AI 对话流式输出:

@GetMapping("/user/aiChat/chat/stream")
public SseEmitter chatStream(@RequestParam String message, @RequestParam Long userId) {
    SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
    
    CompletableFuture.runAsync(() -> {
        try {
            // 调用 Python 流式 API
            pythonClient.chatStream(message, token -> {
                emitter.send(SseEmitter.event().data(token));
            });
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    
    return emitter;
}

附录:面试高频追问

追问应对策略
"你项目有多少用户?"说明是校级系统,重点讲架构设计能力
"为什么不用微服务?"团队规模和业务复杂度不需要,但了解微服务架构
"这个算法是你设计的吗?"是的,基于业务需求分析和算法设计思路
"遇到的最大挑战?"排班算法的公平性保证、JWT + WebSocket 认证
"你还有什么问题?"问团队技术栈、业务方向、实习生培养机制

面试建议

  1. 回答问题时,先说结论,再展开细节
  2. 用项目中的实际例子支撑你的观点
  3. 遇到不会的问题,诚实说不了解,但尝试从已知知识推导
  4. 展示你的思考过程,而不仅仅是最终答案
  5. 对技术保持好奇心,展示你的学习能力

祝大家面试顺利! 🚀

更多推荐