【面经总结】Java基础到进阶+项目融合面试题
适用对象:Java 后端开发 / 实习 / 校招
特色:每道题结合我自身一套全栈管理项目编写而来
生成日期:2026-05-28
目录
- Java 基础
- 集合框架
- 多线程与并发
- JVM 虚拟机
- Spring / Spring Boot
- MySQL 数据库
- Redis 缓存
- MyBatis 持久层
- 设计模式
- 分布式基础
- 场景题 & 综合题
- 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:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(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, SQLException | NPE, 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 |
| finally | try-catch-finally 中的清理代码块 | 关闭资源 |
| finalize | Object 的方法,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 流程:
- 计算 key 的 hash 值:
(h = key.hashCode()) ^ (h >>> 16)(高 16 位与低 16 位异或,减少碰撞) - 计算桶位置:
index = (n - 1) & hash(n 是数组长度,必须是 2 的幂) - 如果桶为空,直接插入新节点
- 如果桶不为空:
- 如果 key 相同,覆盖 value
- 如果是红黑树节点,调用红黑树插入
- 如果是链表,尾插法插入,链表长度 ≥ 8 时转红黑树
- 如果
size > capacity * loadFactor,扩容(2 倍)
JDK 1.7 vs 1.8 优化:
| 方面 | JDK 1.7 | JDK 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):
- 计算 hash,找到桶位置
- 如果桶为空,使用 CAS 插入新节点(无锁)
- 如果桶不为空,使用 synchronized 锁住链表头节点
- 遍历链表,如果 key 存在则覆盖,否则尾插法插入
- 链表长度 ≥ 8 时转红黑树
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:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组(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:
| 特性 | Iterator | ListIterator |
|---|---|---|
| 适用集合 | 所有 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 机制:
- 原理:遍历的是集合的副本或快照,修改不影响迭代
- 实现:
CopyOnWriteArrayList、ConcurrentHashMap - 缺点:无法反映最新的修改
// 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:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | 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() 实际上是三步操作:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
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:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数方向 | 递减到 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) |
| 异常 | OutOfMemoryError | StackOverflowError |
对象创建过程:
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:
- 打开 heapdump.hprof
- 查看 Dominator Tree(支配树),找到占用内存最大的对象
- 查看 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+ 树的三大优势:
- 矮胖结构:每个节点可存多个 key,3 层可存 2000 万数据,IO 次数少
- 叶子节点链表:天然支持范围查询(
WHERE age BETWEEN 20 AND 30) - 非叶子节点不存数据:单个节点可容纳更多 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 COMMITTED | ❌ | ✅ | ✅ | Oracle 默认 |
| REPEATABLE READ | ❌ | ❌ | ✅ | MySQL 默认 |
| 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 |
| 底层实现 | PreparedStatement | Statement |
#{} 的原理:
// 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 二级缓存:
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper(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>
注意:项目中实际上没有使用二级缓存,因为:
- 多表关联查询时,二级缓存容易出现脏数据
- 使用 Redis 手动缓存更灵活可控
- 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 |
优化方案:
- 缓存 Method/Field 对象,避免重复获取
- 使用 MethodHandle(JDK 7+)替代反射
- 使用字节码生成(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:
| 特性 | RDB | AOF |
|---|---|---|
| 原理 | 定时生成内存快照 | 追加写命令日志 |
| 触发方式 | 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:
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向 |
| 协议 | 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 认证 |
| "你还有什么问题?" | 问团队技术栈、业务方向、实习生培养机制 |
面试建议:
- 回答问题时,先说结论,再展开细节
- 用项目中的实际例子支撑你的观点
- 遇到不会的问题,诚实说不了解,但尝试从已知知识推导
- 展示你的思考过程,而不仅仅是最终答案
- 对技术保持好奇心,展示你的学习能力
祝大家面试顺利! 🚀
更多推荐
所有评论(0)