整理时间: 2026-03-17 来源: 牛客网面经抓取 + 面试题库整合 整理规则: 相同/相似知识点合并为一道,保留原始问题描述,全面详细回答
一、Java 基础
1.1 语言特性与概述
1. Java 之外还会其他语言吗?
作为Java后端开发工程师,我主要使用Java进行开发。在学习过程中也接触过其他语言:
C/C++: 学习过C语言基础,了解指针和内存管理的底层概念;C++学习过面向对象特性和STL容器
Python: 用于一些脚本开发和数据处理任务,了解其简洁的语法风格
JavaScript/TypeScript: 了解前端开发基础,能够进行全栈开发
Go: 了解其并发编程模型goroutine和channel机制
不同语言有其适用场景,Java适合企业级后端开发,其强类型、跨平台、丰富的生态等特性使其成为后端开发的主流选择。
2. 介绍一下 Spring AI 这个框架
Spring AI是Spring生态系统中专注于AI应用开发的框架,它提供了统一的标准接口来访问各种大语言模型(LLM)提供商。
核心特性包括:
统一API: 提供了与OpenAI、Azure OpenAI、HuggingFace、Ollama等供应商的统一接口
Prompt模板: 支持动态构建提示词,使用模板引擎将参数注入
输出转换: 自动将LLM输出转换为Java对象(POJO)
RAG支持: 提供了检索增强生成的完整支持,包括文档加载、分割、向量化、向量存储
函数调用: 支持Function Calling,可以动态调用Java方法作为工具
典型使用流程:
添加依赖配置
创建ChatClient
构建Prompt
调用LLM并获取响应
3. 说说你对多态的理解
多态是面向对象三大特性之一(封装、继承、多态),指同一接口表现出不同行为的能力。
Java中多态的实现方式:
方法重写(Override): 子类重写父类方法,运行 时根据实际对象类型决定调用哪个方法
方法重载(Overload): 同一类中方法名相同但参数列表不同,编译时决定
多态的核心条件:
继承关系或接口实现
子类重写父类方法
父类引用指向子类对象(向上转型)
多态的好处:
提高代码的可扩展性和可维护性
符合开闭原则(对扩展开放,对修改关闭)
降低类之间的耦合度
4. 什么是序列化和反序列化?
序列化是将Java对象转换为字节序列的过程,以便能够存储到磁盘文件、数据库或通过网络传输。
反序列化是将字节序列恢复为Java对象的过程。
Java中序列化的实现:
实现
Serializable接口(标记接口)使用ObjectInputStream/ObjectOutputStream进行读写
使用transient关键字标记不需要序列化的字段
序列化的应用场景:
对象持久化到文件或数据库
网络传输对象(RMI、Socket通信)
缓存对象到Redis等缓存中间件
Session序列化(分布式Session)
5. 序列化的原理分析
Java对象序列化原理:
ObjectOutputStream:
写入类元数据(类名、serialVersionUID)
递归写入对象属性
对于引用类型,写入完整的对象图
使用引用机制避免重复序列化
序列化机制:
每个对象关联一个序列化句柄
首次遇到对象时写入完整数据并分配句柄
后续遇到相同对象只写入句柄引用
serialVersionUID:
类的版本标识
序列化时写入,反序列化时校验
不一致会抛出InvalidClassException
Externalizable接口:
完全自定义序列化逻辑
需要实现writeExternal和readExternal方法
6. 为什么需要序列化?
数据持久化: 将对象保存到磁盘、数据库,实现数据持久化存储
网络传输: 网络通信传输的是字节流,序列化将对象转换为字节序列才能传输
缓存: 将对象缓存到Redis等缓存中间件,需要将对象序列化
分布式计算: RMI、Hessian等远程调用技术依赖序列化
Session复制: 分布式环境下Session需要序列化才能在节点间传递
跨语言通信: JSON、XML、ProtoBuf等序列化格式支持跨语言
7. 继承和接口相比,各自的局限性是什么?
继承的局限性:
Java单继承,一个类只能继承一个父类
继承关系是"is-a"关系,可能导致类层次过深
父类变更会影响所有子类,耦合度高
可能导致"钻石问题"(多重继承的二义性)
接口的局限性:
接口只能声明方法签名,不能包含实现(JDK 8之前的default方法)
接口没有状态,只能定义常量
接口实现类必须实现所有方法(default方法除外)
接口变更是破坏性的,实现类需要适配
使用建议:
使用接口定义行为规范,使用抽象类实现通用逻辑
优先使用组合而非继承
尽量使用接口解耦,抽象类用于代码复用
8. Java 中常见的集合类有哪些?分别有什么特点
Collection接口:
List: 有序、可重复
ArrayList: 数组实现,查询快O(1),增删慢O(n),线程不安全
LinkedList: 双向链表实现,增删快O(1),查询慢O(n)
Vector: 数组实现,线程安全,效率低
CopyOnWriteArrayList: 写时复制,读多写少场景
Set: 无序、不重复
HashSet: 基于HashMap,查找O(1),无序
LinkedHashSet: 维护插入顺序
TreeSet: 基于红黑树,有序O(logn)
CopyOnWriteArraySet: 线程安全读多写少
Queue: 队列
PriorityQueue: 优先级队列
ArrayDeque: 双端队列
Map接口: 键值对
HashMap: JDK 1.8数组+链表+红黑树,线程不安全
LinkedHashMap: 维护插入顺序
TreeMap: 红黑树实现,有序
Hashtable: 线程安全,效率低
ConcurrentHashMap: JDK 1.8 CAS+synchronized,线程安全高效
Properties: 继承Hashtable,配置文件
9. 简单介绍一下 Java 中常用的 HashMap,以及 1.8 之后有什么优化
HashMap是Java中最常用的Map接口实现类,基于哈希表实现,提供O(1)的增删改查性能。
JDK 1.8之前的实现:
数组+链表
头插法扩容
链表中元素过多时查找效率降低O(n)
JDK 1.8之后的优化:
数组+链表+红黑树: 当链表长度超过8且数组长度≥64时,链表转换为红黑树,查询效率从O(n)优化到O(logn)
尾插法: 扩容时使用尾插法,避免多线程扩容时形成环形链表导致死循环
扩容优化: 扩容时元素移动只需要看原位置或原位置+旧容量位置,不需要重新计算hash
初始化优化: 使用扰动函数,将hashCode高低位异或,减少hash冲突
红黑树退化: 当红黑树节点数小于等于6时,退化为链表
10. HashMap 的底层实现?
JDK 1.8 HashMap底层数据结构是数组+链表+红黑树的组合。
主要组成部分:
Node[] table: 哈希桶数组,存储链表或红黑树的头节点
Node: 实现Map.Entry接口,包含hash、key、value、next
TreeNode: 继承Node,红黑树节点实现
关键参数:
initialCapacity: 初始容量,默认16
loadFactor: 负载因子,默认0.75
threshold: 扩容阈值 = capacity * loadFactor
TREEIFY_THRESHOLD: 树化阈值,8
UNTREEIFY_THRESHOLD: 退化阈值,6
put过程:
计算key的hash(扰动函数处理)
定位桶位置 (n-1) & hash
遍历链表/红黑树
找到key则更新value,未找到则插入
检查是否需要树化
检查是否需要扩容
11. 往 HashMap 里 put 一个值的过程是怎样的?
hash计算: 调用hash()方法,对key的hashCode进行扰动处理
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }定位索引: 使用(n-1) & hash计算数组下标位置
遍历桶: 如果桶不为空,遍历链表或红黑树
如果找到相同的key,直接更新value
如果未找到,执行插入
插入新节点:
判断是否树化(链表长度≥8且数组长度≥64)
使用尾插法插入链表尾部
modCount++: 修改计数+1
检查扩容: size++后如果大于threshold,执行resize()
JDK 1.7头插法 vs 1.8尾插法:
1.7: 新节点插入头部(头插法)
1.8: 新节点插入尾部(尾插法)
12. HashMap 达到临界值时,扩容是怎么扩容的?
触发条件: 当size > threshold时触发扩容
扩容过程:
新容量 = oldCapacity * 2(翻倍)
新建Node数组
将原数组元素迁移到新数组
元素迁移:
遍历每个桶
对链表/红黑树每个节点重新计算位置
只需判断hash & oldCapacity是否为0
为0保持原位置,为1移动到原位置+oldCapacity位置
性能优化:
迁移过程不需要重新计算hash,利用容量特性判断位置
头节点直接移动
链表拆分为两个链表
13. HashMap 在 JDK1.7 升级到 1.8 做过哪些优化?
数据结构优化: 数组+链表 → 数组+链表+红黑树
链表过长时查找效率O(n) → 红黑树O(logn)
插入方式优化: 头插法 → 尾插法
避免多线程扩容时形成环形链表导致死循环
hash函数优化:
扰动函数处理,使hash分布更均匀
高位参与运算,减少碰撞
扩容优化:
不需要重新计算hash
直接判断hash & oldCapacity决定新位置
初始化优化:
延迟初始化,首次put时才创建数组
树化/退化条件:
树化: 链表长度≥8且数组长度≥64
退化: 红黑树节点数≤6
14. HashMap 在 JDK 8 下为什么要引入红黑树,树化与退化的条件是什么?
引入红黑树的原因:
当hash冲突严重时,链表会很长
链表查找是O(n),影响性能
红黑树是自平衡二叉查找树,查找O(logn)
在链表长度较长时,显著提升查找效率
树化条件:
链表长度 >= 8
数组长度 >= 64
两个条件必须同时满足才会树化
如果数组长度 < 64,会先扩容而非树化
退化条件:
红黑树节点数 <= 6
退化为链表,避免频繁树化/退化带来的开销
为什么选择8作为阈值:
红黑树适合动态插入删除,链表适合静态查询
8是一个经验值,链表长度超过8时,红黑树的log(8)=3 < 链表遍历平均次数
退化阈值设为6,避免频繁转换
15. 红黑树相对于链表有什么优点
查询效率高:
链表: O(n)
红黑树: O(logn)
当数据量大时,差异明显
自平衡:
红黑树通过着色和旋转保持近似平衡
保证最长路径不超过最短路径的2倍
查找、插入、删除在最坏情况下都是O(logn)
适合范围查询:
红黑树是有序的,支持范围查询
链表不支持
空间换时间:
红黑树节点需要存储颜色、左右子树指针
但换取了更高的查询效率
适用场景:
数据量大、查询频繁
需要有序遍历
插入删除不特别频繁
16. 红黑树和AVL树有什么区别?适用场景有什么不同?
AVL树:
严格平衡,任意节点左右子树高度差不超过1
查找效率更稳定O(logn)
插入/删除可能需要多次旋转
适合查找密集型场景
红黑树:
近似平衡,最长路径不超过最短路径2倍
插入/删除旋转次数少
适合插入删除频繁场景
区别对比:
适用场景:
AVL: 数据库索引(查找多,修改少)
红黑树: 内存数据结构(Java HashMap、TreeMap、Linux进程调度)
17. JDK1.7 头插法为什么会出现死循环?
在JDK 1.7中,HashMap扩容采用头插法,多线程场景下会导致死循环。
原因分析:
线程A和B同时触发扩容
假设链表为 A → B → null(头节点A,尾节点B)
线程A执行transfer(),将链表反转:B → A → null
线程B也执行transfer()
由于头插法,线程B会创建新链表
由于Entry的next指针已被修改,形成环形链表
死循环过程:
原链表: A -> B -> C
线程A处理: C -> B -> A (反转)
线程B处理时,遍历到A,发现A.next=B,B.next=A,形成环后果:
get()操作可能死循环CPU 100%
内存泄漏
数据丢失
18. 1.8 为什么改成尾插法?
避免死循环:
尾插法保证插入顺序
多线程扩容时不会形成环形链表
数据一致性:
头插法会反转链表,导致数据丢失
尾插法保持原有顺序
但仍非线程安全:
尾插法解决了死循环问题
但仍存在数据覆盖等问题
多线程场景应使用ConcurrentHashMap
19. const int p和int const p有什么区别?
这是C/C++的指针语法问题:
const int p (或 int const p):
p是一个指向常量整数的指针
不能通过p修改所指向的值
可以重新赋值p指向其他地址
"const修饰的是int"
int * const p:
p是一个常量指针,指向整数
可以通过p修改所指向的值
不能重新赋值p指向其他地址
"const修饰的是p"
const int * const p:
两者都不能修改
p的指向固定,内容也不能修改
20. Java 中 int 的取值范围的最小值是多少?
Java中int是32位有符号整数:
最大值: 2^31 - 1 = 2147483647 (Integer.MAX_VALUE)
最小值: -2^31 = -2147483648 (Integer.MIN_VALUE)
特殊说明:
Integer.MIN_VALUE = -2147483648
但-2147483648这个字面量在代码中实际上是编译器特殊处理的
Integer.MIN_VALUE - 1 会发生溢出,结果是 Integer.MAX_VALUE
这是因为补码表示中,-2147483648没有对应的正数表示
21. 数组长度扩容两倍,里面的数据会怎么变化?
这个问题需要区分不同上下文:
Java中数组:
数组长度一旦创建不可改变
"扩容"实际上是创建新数组,将原数据复制过去
Arrays.copyOf() 内部使用System.arraycopy()
所有元素都会被复制到新数组
HashMap扩容:
新容量 = oldCapacity * 2
元素重新计算位置:(n-1) & hash
每个元素要么在原位置,要么在原位置+oldCapacity位置
不需要重新计算hash
ArrayList扩容:
新容量 = oldCapacity + oldCapacity/2 (1.5倍)
使用Arrays.copyOf复制元素
22. 链表中删除指定节点时,时间复杂度是多少(仅说单链表场景)?
单链表删除节点:
已知节点引用(不带头节点):
删除当前节点需要找到前驱节点
最坏O(n),因为需要遍历找到前驱
最好O(1),如果删除的是第一个节点
已知前驱节点:
直接通过前驱节点的next指针删除
O(1)
双向链表:
无论是否知道前驱,直接操作指针
O(1)
注意:
单链表删除操作本身是O(1)
关键是能否直接找到要删除的节点或前驱节点
23. 归并排序的空间复杂度是多少,核心优势是什么?
空间复杂度: O(n)
需要一个与原数组等大的临时数组
递归调用栈深度: O(log n)
总空间: O(n + log n) = O(n)
核心优势:
稳定性: 归并排序是稳定的排序算法,相等元素的相对顺序不变
时间复杂度稳定: 最好、最坏、平均都是O(n log n)
不受数据分布影响
适合大规模数据排序
适合外排序:
适合处理大规模文件的排序
可以逐段读入内存处理
适合链表:
链表可以通过修改指针实现O(1)空间的归并排序
并行友好:
归并过程可以并行处理
适合多核并行计算
24. 冒泡排序的时间复杂度是多少,什么场景下效率最低?
时间复杂度:
最好: O(n) (优化后,加了提前终止标志)
平均: O(n²)
最坏: O(n²)
效率最低的场景:
完全逆序数组: 每一轮都要进行完整比较,交换次数最多
基本有序但非升序: 如 [2,1,3,4,5,6,7,8,9],需要多轮冒泡
数据量大的随机数组: O(n²)复杂度在数据量大时性能很差
冒泡排序特点:
简单直观,稳定排序
空间O(1)
适合数据量小、基本有序的场景
实际生产中很少使用
25. 快速排序的最坏时间复杂度是多少,什么场景会触发?
最坏时间复杂度: O(n²)
触发场景:
数组基本有序:
每次划分极度不平衡
如 [1,2,3,4,5,6,7,8] 升序数组
每次pivot都是最大/最小值
数组完全逆序:
同理,每次划分一边为空
大量重复元素:
所有元素相等
每次划分都极度不平衡
优化方案:
随机选择pivot: 避免特定数据模式的最坏情况
三数取中: 选择left、mid、right三个数的中值作为pivot
双pivot快排: JDK 1.7之后对基本类型使用双pivot快排
小数组使用插入排序: 数据量小时,插入排序更快
26. 什么是内存泄漏?如何检测和避免内存泄漏?
内存泄漏: 程序分配内存后,无法释放已不再使用的内存,导致内存占用持续增长。
常见原因:
静态集合类: 静态Map/Set持有对象引用
单例模式: 生命周期与应用一致,持有大量对象
未关闭的资源: 数据库连接、网络连接、文件流未关闭
监听器/回调: 注册监听器但未取消注册
ThreadLocal: 未调用remove()导致内存泄漏
内部类引用外部类: 非静态内部类持有外部类引用
缓存: 缓存数据无限增长
字符串常量池: 大量String.intern()调用
检测方法:
VisualVM: 监控内存使用趋势
MAT (Memory Analyzer Tool): 分析堆转储文件
JProfiler: 专业Java性能分析工具
JConsole: JDK自带监控工具
GC日志: 分析GC频率和内存回收情况
避免策略:
使用完资源及时释放(finally/try-with-resources)
合理使用WeakHashMap、SoftReference
避免long-lived对象持有大量引用
使用ThreadLocal后调用remove()
定期进行内存分析
27. vector和list的底层数据结构有什么不同?分别适用于什么场景?
vector:
底层是动态数组
连续内存存储,支持随机访问
插入/删除在末尾O(1),在中间O(n)
适合: 随机访问频繁,尾部插入删除为主
list:
底层是双向链表
非连续内存,通过指针连接
任意位置插入删除O(1)
不支持随机访问,访问需要遍历
适合: 频繁的中间插入删除,顺序遍历
C++ STL中的选择:
vector: 默认首选,性能更好,内存连续,缓存友好
list: 需要频繁在中间插入删除,或迭代器稳定性要求高
Java对应:
vector对应ArrayList(但vector是线程安全的)
list对应LinkedList
28. 写时拷贝(Copy-On-Write)的应用场景和好处是什么?
Copy-On-Write (COW):
写入时不直接修改原数据
先复制一份,在副本上修改
修改完成后,用副本替换原数据
应用场景:
操作系统: fork()进程时,父子进程共享内存,写时复制
Java集合: CopyOnWriteArrayList、CopyOnWriteArraySet
数据库: MVCC实现
Git: 文件系统快照
函数式编程: 不可变数据结构
好处:
读多写少场景性能好: 读操作无需加锁,可以并发读
实现简单: 无需复杂的锁机制
数据一致性: 写操作不会影响读取
无锁编程: 提高并发性能
缺点:
每次写都需要复制,内存开销大
写操作频繁时性能差
内存占用高,可能引发GC
适用场景:
读操作远多于写操作
对数据一致性要求不高
缓存、配置等场景
29. 介绍一下你做过的最有挑战性的项目,遇到了什么技术难点,如何解决的?
我做过最有挑战性的项目是一个高并发抽奖与发奖系统,峰值流量集中在活动开始后的前3分钟,核心难点是“高并发下不超卖 + 发奖链路不重复 + 异常可恢复”。
我的处理方法:
库存一致性: Redis + Lua 原子扣减,避免并发超卖
发奖异步化: 下单与发奖解耦,通过MQ削峰
消费幂等: 业务唯一键 + 去重表,防止重复发奖
补偿机制: 失败消息重试 + 定时对账修复
可观测性: 增加全链路traceId、失败率告警
结果:
峰值期间接口成功率稳定在99.9%+
核心接口P99由约800ms降至200ms内
重复发奖问题基本消除(通过幂等保障)
30. 请用一句话概括你参与过的核心项目的核心业务场景。
我参与的是一个高并发活动平台,负责“用户实时请求 -> 风控校验 -> 库存扣减 -> 异步发奖 -> 最终一致落库”的核心链路建设与稳定性保障。
31. 项目中你负责的模块如何保证接口的幂等性?
接口幂等性指同一请求多次执行与执行一次的效果相同。
保证幂等性的方案:
数据库唯一约束:
使用唯一索引防止重复插入
业务单号+唯一索引
分布式锁:
Redis SETNX实现
锁的key=接口+业务参数
锁过期时间防止死锁
状态机:
订单状态流转:创建→支付→发货→完成
只有正确状态才能执行对应操作
去重表:
记录已处理的请求ID
处理前先查询是否已处理
Token机制:
前端获取Token,后端验证
验证成功后删除Token
32. 你遇到印象最深的问题是什么?怎么解决的?
我印象最深的问题是一次线上高峰期出现“缓存击穿 + 慢SQL叠加”导致接口雪崩。
处理过程:
问题现象: 接口RT从毫秒级上升到秒级,超时率持续上升
排查过程: 先看监控发现DB连接池打满,再查慢日志和Redis命中率
根因分析: 热点key过期后大量请求直接回源,叠加一条缺索引SQL
解决方案: 热点key互斥重建 + 过期时间随机化 + SQL加索引 + 限流降级
复盘结果: 建立上线前压测门禁与缓存预热流程
33. 分享一个你印象最深的 Bug 吧~当时现象是啥?怎么一步步定位到根因的?最后怎么解决的?从中学到了啥?
示例(可直接复述):
现象: 活动开始后,用户偶发看到“重复中奖”
定位步骤:
查业务日志,发现同一用户同一活动短时间内出现两条成功记录
查MQ消费日志,发现存在消息重投递
查数据库,确认缺少业务唯一约束
根因: 消费端“至少一次投递”语义下未做严格幂等
修复:
增加
(activity_id, user_id, biz_no)唯一索引消费逻辑改为“先幂等校验再执行业务”
异常重试采用指数退避,避免瞬时放大
经验: 任何异步链路都要按“会重复、会乱序、会失败”设计
34. 你编程主要在什么操作系统下进行的?
主要在Windows和Linux下进行开发:
Windows: 开发环境,IDE使用
Linux: 服务器环境,测试环境,Docker容器
常用工具:
IDE: IntelliJ IDEA, VS Code
终端: Git Bash, WSL, Xshell
版本控制: Git
35. Windos和Linux操作系统的区别是什么?
1.2 并发编程
53. 进程 线程的区别 深入讲一下
进程:
操作系统资源分配的基本单位
拥有独立的地址空间
包含代码、数据、文件、内存等资源
进程间通信需要IPC机制
切换开销大(需要切换页表等)
线程:
CPU调度的基本单位
共享进程的地址空间
拥有独立的栈和寄存器
线程间通信简单(共享内存)
切换开销小
进程与线程的关系:
一个进程可以包含多个线程
线程是进程内的执行流
线程共享进程的资源
Java中的线程:
Java线程与OS线程一一映射
线程是抢占式调度
线程安全需要自己保证
54. 说说进程和线程的区别,以及它们的通信方式
进程间通信方式:
管道/命名管道: 父子/兄弟进程间通信
消息队列: 消息链表,异步通信
共享内存: 最高效的进程间通信方式
信号量: 计数器,控制资源访问
信号: 异步通知机制
套接字(Socket): 网络通信,也可用于本地
文件映射: 共享文件区域
线程间通信方式:
共享变量: 共享进程的内存
wait/notify: Object方法
Lock/Condition: JUC包
volatile: 保证可见性
ThreadLocal: 线程本地变量
阻塞队列: 生产者-消费者模式
CountDownLatch/CyclicBarrier: 同步工具
55. 说说进程和线程的区别,以及线程间通信的方式有哪些?
(与上题类似,补充线程间通信方式)
线程间通信方式详解:
volatile变量:
保证可见性
适合简单状态标志
synchronized + wait/notify:
最基础的线程通信方式
wait()释放锁,notify()唤醒
ReentrantLock + Condition:
更灵活的等待/通知机制
可以精确唤醒指定线程
CountDownLatch:
倒计时器
等待一组线程完成
CyclicBarrier:
栅栏
一组线程相互等待
Semaphore:
信号量
控制资源访问数量
阻塞队列:
BlockingQueue
生产者-消费者模式
CompletableFuture:
异步编程
任务编排
56. 线程之间哪些内存是共享的?
线程共享的内存区域:
堆: 所有线程共享,存放new的对象
方法区/元空间: 所有线程共享,存放类信息、静态变量、常量
字符串常量池: 堆中的一部分
线程私有的内存区域:
虚拟机栈: 每个线程私有,存放局部变量、操作数栈
本地方法栈: 与虚拟机栈类似,服务于native方法
程序计数器: 记录当前线程执行的字节码行号
注意:
栈是线程私有的,不存在线程安全问题
堆是共享的,需要考虑线程安全
volatile保证可见性,但不能保证原子性
57. 多线程共享数据出现线程安全问题的核心原因是什么?
核心原因: 多个线程同时访问共享资源,且至少有一个线程在修改资源。
具体分析:
原子性问题:
复合操作不是原子操作
如 i++ 需要:读→改→写 三步
可能被其他线程打断
可见性问题:
线程修改后未立即刷新到主内存
其他线程读取到的是旧值
有序性问题:
指令重排序导致执行顺序改变
破坏多线程协作
产生条件:
多个线程并发
访问同一共享资源
至少有一个线程在修改
解决方案:
原子性: synchronized, Lock, CAS
可见性: volatile, synchronized, Lock
有序性: volatile, synchronized
58. 多线程共享数据时,使用 synchronized 关键字的核心作用是什么?
synchronized的核心作用:
原子性保证: 保证同一时刻只有一个线程执行同步代码块
可见性保证: 释放锁前会将工作内存刷新到主内存
有序性保证: 防止指令重排序
用法:
// 1. 修饰实例方法
public synchronized void method() {}
// 2. 修饰静态方法
public synchronized static void method() {}
// 3. 修饰代码块
synchronized(obj) {}锁的对象:
实例方法: this对象
静态方法: Class对象
代码块: 指定的对象
底层原理:
JVM层面实现
通过monitorenter/monitorexit指令
自动获取和释放锁
59. CAS 是什么?
CAS (Compare And Swap):
比较并交换,是一种无锁算法
包含三个操作数:内存位置V、预期值A、新值B
只有当V的值等于A时,才将V更新为B
Java中的CAS:
位于java.util.concurrent.atomic包
基于Unsafe类的native方法实现
实现原理:
// 伪代码
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}CAS的应用:
AtomicInteger、AtomicLong等原子类
ConcurrentHashMap的CAS操作
AQS中的CAS操作
CAS的优缺点:
优点: 非阻塞,性能高
缺点: ABA问题、自旋开销、只能保证单变量
60. ThreadLocal的原理及在项目中的作用
ThreadLocal原理:
为每个线程提供独立的变量副本
内部通过ThreadLocalMap实现
key是ThreadLocal实例,value是存储的值
每个Thread对象都有一个ThreadLocalMap
ThreadLocalMap:
类似HashMap的数据结构
Entry继承WeakReference<ThreadLocal<?>>
key是弱引用,value是强引用
项目中的作用:
线程隔离: 每个线程有自己的用户信息
事务管理: Spring的事务同步器
日期格式化: SimpleDateFormat线程不安全,用ThreadLocal解决
链路追踪: MDC日志追踪
使用示例:
// 存储用户信息
ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(currentUser);
User user = userThreadLocal.get();
userThreadLocal.remove(); // 使用完必须移除61. ThreadLocalMap 的底层数据结构是什么,为什么要采用这种结构?
底层数据结构:
类似于HashMap的数组+链表/红黑树
数组: Entry[] table
初始化容量: 16
负载因子: 2/3
Entry结构:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}采用弱引用的原因:
key是ThreadLocal的弱引用
当ThreadLocal对象不再被外部引用时,可以被GC回收
避免ThreadLocal对象长期存活导致内存泄漏
防止线程长期存活时ThreadLocalMap不断膨胀
为什么不用强引用:
如果用强引用,ThreadLocal对象无法被GC回收
即使ThreadLocal不再使用,也会占用内存
注意:
虽然key是弱引用,但value是强引用
使用完ThreadLocal后必须调用remove(),否则可能内存泄漏
62. TransmittableThreadLocal 相比 InheritableThreadLocal,核心解决了什么问题?
InheritableThreadLocal的问题:
只能在父子线程间传递
不能在线程池、异步任务等场景传递
子线程创建时复制父线程的值,但之后修改不会同步
TransmittableThreadLocal (TTL):
Alibaba开源的库
核心解决:线程池、异步任务场景下的ThreadLocal传递
解决的问题:
线程池场景: 主线程设置值,线程池中的线程能获取到
CompletableFuture: 异步任务能获取主线程的ThreadLocal
ForkJoinPool: 子任务能获取父任务的ThreadLocal
消息队列消费者: 消费线程能获取设置的值
原理:
任务提交时,捕获当前ThreadLocal快照
任务执行时,恢复快照到工作线程
任务结束后,清理恢复的ThreadLocal
63. synchronized 和 ReentrantLock 的区别?
ReentrantLock优势:
可中断等待
可设置公平锁
可以尝试获取锁
多个条件变量
锁投票
synchronized优势:
语法简洁
JVM自动管理
性能已优化
选择建议:
简单同步用synchronized
复杂场景用ReentrantLock
64. volatile 的作用?
保证可见性:
线程修改后立即刷新到主内存
其他线程读取到最新值
禁止指令重排序:
插入内存屏障
防止重排序导致的问题
不保证原子性:
如i++这种复合操作无法保证
需要用synchronized或AtomicInteger
使用场景:
状态标志
双重检查锁定
可见性要求高的变量
65. volatile 和 synchronized 的区别?
volatile适用场景:
状态标志
双重检查锁定
读写分离场景的写操作
synchronized适用场景:
需要保证原子性的场景
复杂同步逻辑
66. 线程WAITING状态和TIMED_WAITING状态最核心的区别是什么?
WAITING状态:
无限期等待,必须被其他线程唤醒
调用wait()、join()(不带超时)
LockSupport.park()
TIMED_WAITING状态:
限时等待,时间到自动返回
Thread.sleep()
wait(timeout)
join(timeout)
LockSupport.parkNanos()
Condition.await(timeout)
核心区别:
WAITING: 等待时间不确定,依赖其他线程通知
TIMED_WAITING: 等待时间确定,到时间自动唤醒
实际影响:
WAITING状态的线程不会被超时中断
TIMED_WAITING可以通过超时自动唤醒
67. 线程池在任务堆积、队列满、拒绝策略触发时的底层执行流程是什么?
线程池执行流程:
提交任务
↓
判断核心线程数是否已满?
↓ 是 ↓ 否
↓ 创建核心线程执行任务
↓
判断阻塞队列是否已满?
↓ 是 ↓ 否
↓ 进入队列等待
↓
判断最大线程数是否已满?
↓ 是 ↓ 否
↓ 创建非核心线程执行任务
↓
执行拒绝策略拒绝策略:
AbortPolicy (默认): 抛RejectedExecutionException
CallerRunsPolicy: 由调用方线程执行
DiscardPolicy: 静默丢弃
DiscardOldestPolicy: 丢弃最老的任务
队列满的场景:
任务提交速度 > 处理速度
队列设置过小
线程处理速度过慢
68. 100个进程,5个并发数的工作,应该怎么设计?
设计思路:
使用线程池控制并发
100个任务,5个并发线程
方案一:线程池+CountDownLatch
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
final int taskId = i;
executor.submit(() -> {
try {
process(taskId);
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();方案二:信号量
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 100; i++) {
semaphore.acquire();
executor.submit(() -> {
try {
process(i);
} finally {
semaphore.release();
}
});
}69. 详细说说父子进程,包括内存地址、共享页表、写时拷贝、子进程回收
父子进程:
fork()创建子进程
父子进程地址空间独立
共享父进程的资源
写时拷贝(Copy-On-Write):
fork()时不立即复制内存
父子进程共享同一物理页
只有当某进程写入时,才复制该页
节省内存,提高效率
共享页表:
父子进程共享父进程的页表
页表项指向相同的物理页
写入时触发缺页中断,分配新页
进程回收:
父进程需要wait()子进程
防止僵尸进程
僵尸进程: 进程已退出但父进程未回收
70. 你是如何避免获奖数据重复插入的,核心解决思路是什么?
问题场景: 抽奖活动可能因为网络重试、并发导致重复插入
解决方案:
数据库唯一约束:
对业务单号加唯一索引
重复插入会报错
分布式锁:
Redis SET lock{活动ID}{用户ID} NX EX
获取锁后才执行业务
前置查询:
插入前先查询是否存在
存在则返回已有结果
幂等设计:
使用业务单号作为幂等标识
相同单号只处理一次
71. 项目中处理抽奖异步逻辑时,使用的具体异步框架是什么?
常见选择:
ThreadPoolExecutor:
自定义线程池
灵活配置参数
CompletableFuture:
Java 8异步编程
支持任务编排
消息队列:
RabbitMQ
RocketMQ
Kafka
分布式任务调度:
XXL-Job
ElasticJob
Spring Task
选择依据:
实时性要求
可靠性要求
复杂度考量
1.3 Java 核心
72. Java 多态的实现方式中,方法重写和方法重载最核心的区别是什么?
核心区别:
重写: 运行时多态,依赖继承关系
重载: 编译时多态,参数列表区分
73. Java 封装特性在你项目中的具体体现是什么(仅说一个核心场景)?
示例:用户模块的封装
属性私有化: private修饰属性
提供getter/setter: 控制访问
业务逻辑封装: 在setter中加入校验逻辑
对外暴露服务: 通过Service层提供
public class User {
private String name;
private int age;
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法");
}
this.age = age;
}
}74. 你用 Java 创建线程时,继承 Thread 类和实现 Runnable 接口最核心的区别是什么?
核心区别:
Runnable是任务,Thread是执行任务的载体
Runnable可以实现资源共享
Java单继承的限制
最佳实践:
实现Runnable接口
使用线程池
75. Java 中运行时异常(RuntimeException)和受检异常(Checked Exception)最核心的区别是什么?
核心区别:
RuntimeException: 编程错误,应该通过代码避免
CheckedException: 需要外部处理,IO、网络等
最佳实践:
不要过度使用CheckedException
优先使用RuntimeException
在边界进行统一异常处理
76. 方法不是 public
(问题不完整,可能指方法访问修饰符相关)
Java访问修饰符:
public: 公开的
protected: 同包或子类
default(无): 同包可见
private: 私有
77. 你是用 workflow 方式搭建的 agent 吗?
(根据项目实际情况回答)
如果使用工作流方式:LangChain、Spring AI等
如果使用纯LLM调用:直接API调用
工作流方式的优点:
流程可控
易于调试
可嵌入业务逻辑
78. 用 Spring AI 写一个 agent 的过程大概是什么样的?
添加依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter</artifactId> </dependency>配置:
配置API Key
选择模型
创建ChatClient:
ChatClient chatClient = ChatClient.builder(chatModel).build();构建Prompt:
使用PromptTemplate
定义输入参数
调用:
String response = chatClient.prompt() .user("问题") .call() .content();
79. 整个过程完全是大模型自己决策吗?
不是完全自动,通常需要:
Prompt工程: 人工设计提示词
Function Calling: 接入外部工具
工作流编排: 控制执行流程
结果校验: 验证输出正确性
典型架构:
LLM负责理解和生成
人类负责设计流程和校验
80. 还接触过其他 Agent 开发框架吗?
LangChain:
Python为主
丰富的组件生态
LangChain4j:
Java版的LangChain
简化LLM集成
LlamaIndex:
专注于RAG
AutoGen:
Microsoft开源
多Agent协作
81. JDK 动态代理与 CGLIB 动态代理的底层实现差异是什么?
JDK动态代理:
要求目标类实现接口
基于Java反射机制
运行时生成$Proxy0类
实现相同接口
调用InvocationHandler
CGLIB动态代理:
不需要接口
基于ASM字节码修改
运行时生成目标类的子类
重写方法添加逻辑
使用MethodInterceptor
对比:
82. 什么是模板元编程?有什么实际应用?
模板元编程(Template Metaprogramming):
使用模板在编译时生成代码
C++的模板特化、偏特化
可以在编译时计算值
Java中的应用:
泛型: 编译时类型擦除
注解处理器: 编译时生成代码
Lombok: 使用注解处理器生成代码
C++模板示例:
template<int n>
struct Factorial {
static const int value = n * Factorial<n-1>::value;
};
template<>
struct Factorial<0> {
static const int value = 1;
};83. 说说C++的模板编程,模板的优缺点是什么?
C++模板:
泛型编程的基础
函数模板和类模板
模板特化和偏特化
优点:
代码复用
类型安全
性能高(编译时生成代码)
灵活性高
缺点:
编译时间长
代码膨胀
编译错误信息难懂
学习曲线陡峭
二、MySQL 数据库
2.1 索引原理
84. MySQL中的外连接和内连接
内连接(INNER JOIN):
只返回两个表中匹配的行
不匹配的行不返回
外连接(OUTER JOIN):
返回匹配的行,还返回不匹配的行
LEFT JOIN: 左表所有行 + 右表匹配行
RIGHT JOIN: 右表所有行 + 左表匹配行
FULL JOIN: 左右表所有行(MySQL不支持)
语法示例:
-- 内连接
SELECT * FROM A INNER JOIN B ON A.id = B.id;
-- 左外连接
SELECT * FROM A LEFT JOIN B ON A.id = B.id;85. MySQL中索引的作用和类型有哪些?
索引的作用:
加快数据检索速度
减少IO操作
加速表连接
加速排序和分组
索引类型:
按数据结构:
B+树索引(默认)
哈希索引(Memory引擎)
全文索引(Full-text)
按物理存储:
聚簇索引(Clustered)
非聚簇索引(Secondary)
按字段个数:
单列索引
联合索引
按特性:
主键索引(唯一且非空)
唯一索引
普通索引
86. 数据库有哪些索引类型?B+树索引的原理是什么?
索引类型:
B+树索引: MySQL默认
哈希索引: 等值查询,范围查询不支持
全文索引: 文本搜索
空间索引: GIS数据
B+树原理:
是一种多路平衡查找树
只有叶子节点存储数据
叶子节点之间通过双向链表连接
所有数据都存在叶子节点
为什么是B+树:
减少IO次数(矮胖)
范围查询友好(链表)
查询稳定(所有数据在叶子)
87. MySQL 索引底层是什么结构?
InnoDB引擎:
使用B+树作为索引结构
聚簇索引: 主键索引,叶子节点存储完整数据
非聚簇索引: 二级索引,叶子节点存储主键值
B+树特点:
多路平衡查找树
非叶子节点不存储数据
叶子节点存储数据
叶子节点双向链表
88. B+树的特点是什么?
多路平衡:
每个节点可以有多个子节点
路径长度相同
只叶子存储数据:
非叶子节点只存储索引
叶子节点存储完整数据
叶子节点链表:
叶子节点通过双向链表连接
范围查询高效
查询稳定:
无论查询哪条数据,路径长度相同
IO次数可预测
89. 为什么用 B+ 树而不是 B 树?
B树 vs B+树:
选择B+树的原因:
查询性能稳定
范围查询高效
空间利用率高
更适合磁盘存储
90. SQL 索引的核心作用是解决什么问题?
核心作用: 加快数据检索速度
解决的问题:
全表扫描: 从O(n)降低到O(log n)
排序: 避免filesort
分组: 避免临时表
连接: 加速多表关联
代价:
占用额外空间
降低写操作性能
需要维护成本
91. 如何检查SQL是否使用了覆盖索引?
覆盖索引:
查询的所有列都在索引中
无需回表查询
检查方法:
EXPLAIN SELECT id, name, age FROM user WHERE name = 'Tom';关键字段:
type: const, ref, range等(不是ALL)
Extra: Using index(表示使用了覆盖索引)
Extra: Using index condition(可能回表)
示例:
-- 覆盖索引
CREATE INDEX idx_name_age ON user(name, age);
SELECT name, age FROM user WHERE name = 'Tom';
-- 非覆盖索引
SELECT * FROM user WHERE name = 'Tom';92. MySQL 联合索引的生效规则中,最关键的一点是什么?
最关键点: 最左前缀原则
含义:
联合索引(a, b, c)
可以使用索引的情况: a | a,b | a,b,c
不能使用索引: b | b,c | c
示例:
CREATE INDEX idx_a_b_c ON table(a, b, c);
-- 走索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
-- 不走索引
WHERE b = 2
WHERE b = 2 AND c = 3
WHERE c = 3注意:
顺序必须正确
遇到范围查询(w>, <, like)会停止匹配
93. MySQL 中聚簇索引和非聚簇索引的核心区别是什么?
聚簇索引:
叶子节点存储完整数据行
每张表只能有一个
通常是主键索引
数据与索引存储在一起
非聚簇索引:
叶子节点存储主键值
每张表可以有多个
二级索引都是非聚簇索引
需要回表查询完整数据
区别对比:
94. InnoDB 引擎中,聚簇索引的底层存储结构及查询流程是什么?
存储结构:
B+树结构
叶子节点存储完整数据行
主键作为索引键
主键值唯一标识每行
查询流程:
根据查询条件定位索引
如果是主键查询,直接从聚簇索引获取数据
如果是二级索引,先查二级索引获取主键,再用主键查聚簇索引(回表)
InnoDB行格式:
Compact行格式
变长字段长度列表
NULL值列表
事务ID和回滚指针(隐藏列)
实际数据
95. MySQL 索引下推(ICP)的核心作用是什么?
索引下推(Index Condition Pushdown):
在索引遍历过程中,对索引包含的字段进行过滤
减少回表次数
示例:
CREATE INDEX idx_name_age ON user(name, age);
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;ICP作用:
不使用ICP: 先按name找到所有记录,再过滤age
使用ICP: 在遍历索引时同时检查age,减少回表
开启方式:
MySQL 5.6+默认开启
EXPLAIN中查看Using index condition
96. 索引什么时候会失效?
不满足最左前缀:
-- 索引 idx(a,b,c) WHERE b = 1 -- 失效使用函数或计算:
WHERE LOWER(name) = 'tom' -- 失效 WHERE age + 1 = 20 -- 失效类型转换:
WHERE name = 123 -- 隐式转换,失效使用LIKE通配符:
WHERE name LIKE '%张' -- 失效 WHERE name LIKE '%张%' -- 失效OR连接:
WHERE a = 1 OR b = 2 -- 如果没有组合索引,失效不等于:
WHERE age != 20 -- 可能失效IS NULL:
WHERE name IS NULL -- 可能失效
97. 你写 SQL 时,如何避免 JOIN 多表后出现笛卡尔积的问题?
笛卡尔积: 两表每行都匹配
避免方法:
明确连接条件:
-- 正确 SELECT * FROM A JOIN B ON A.id = B.a_id; -- 错误,会产生笛卡尔积 SELECT * FROM A, B;使用INNER JOIN而非CROSS JOIN:
-- 避免 SELECT * FROM A CROSS JOIN B;先过滤再连接:
SELECT * FROM (SELECT * FROM A WHERE ...) a JOIN (SELECT * FROM B WHERE ...) b ON a.id = b.a_id;检查数据关联:
确认关联字段是否正确
确认是否有一对多的关系
98. MySQL 中左连接(LEFT JOIN)查询时,ON 和 WHERE 条件的核心区别是什么?
ON条件:
控制如何连接两个表
在生成连接结果时起作用
左连接会保留左表所有行
WHERE条件:
对连接后的结果进行过滤
在连接完成后起作用
会影响最终结果集
示例:
-- ON: 连接条件
SELECT * FROM A LEFT JOIN B ON A.id = B.id AND B.status = 1;
-- 即使B.status != 1,A的数据也会保留
-- WHERE: 过滤条件
SELECT * FROM A LEFT JOIN B ON A.id = B.id WHERE B.status = 1;
-- B.status != 1的行会被过滤掉2.2 事务与隔离级别
99. 详细说说MySQL的事务机制
事务特性(ACID):
原子性(Atomicity):
事务是最小执行单位
要么全部成功,要么全部失败
通过undo log实现
一致性(Consistency):
事务执行前后,数据库状态一致
通过其他特性保证
隔离性(Isolation):
并发事务互不干扰
通过锁和MVCC实现
持久性(Durability):
事务提交后,修改永久保存
通过redo log实现
事务控制:
START TRANSACTION;
-- SQL语句
COMMIT; -- 提交
ROLLBACK; -- 回滚100. 数据库事务的隔离级别有哪些?分别解决什么问题?
四个隔离级别:
READ UNCOMMITTED (读未提交):
问题: 脏读、不可重复读、幻读
解决: 无
READ COMMITTED (读已提交):
解决脏读
问题: 不可重复读、幻读
REPEATABLE READ (可重复读):
解决脏读、不可重复读
问题: 幻读(InnoDB通过MVCC解决)
SERIALIZABLE (串行化):
解决所有问题
性能最差
解决的问题:
脏读: 读取到其他事务未提交的数据
不可重复读: 同一事务两次读取结果不同
幻读: 同一事务两次查询结果数量不同
101. 事务的隔离级别有哪些?
MySQL常见四种隔离级别:
READ UNCOMMITTED(读未提交)
可能出现:脏读、不可重复读、幻读
并发最高,一般不用于生产核心交易
READ COMMITTED(读已提交)
解决脏读
仍可能出现不可重复读、幻读
Oracle默认级别
REPEATABLE READ(可重复读)
解决脏读、不可重复读
InnoDB默认级别,配合MVCC与间隙锁可抑制幻读
SERIALIZABLE(串行化)
最强隔离,基本避免并发异常
吞吐最低,适合极少数强一致关键场景
生产建议:
大多数OLTP业务使用
REPEATABLE READ或READ COMMITTED不要盲目升到
SERIALIZABLE,会显著降低吞吐
102. 幻读是什么?如何解决?
幻读:
事务A读取到了事务B提交的新增数据
两次查询结果集数量不同
示例:
-- T1: SELECT * FROM user WHERE age > 10; -- 0条
-- T2: INSERT INTO user VALUES(1, 'Tom', 20); COMMIT;
-- T1: SELECT * FROM user WHERE age > 10; -- 1条(幻读)解决方案:
SERIALIZABLE:
串行化,最严格
可重复读 + Gap锁:
InnoDB的Next-Key锁
锁住记录之间的间隙
防止插入新记录
MVCC:
读使用快照
解决读幻读
写仍需锁
103. MySQL 中 MVCC 机制的底层实现原理是什么?
MVCC (Multi-Version Concurrency Control):
多版本并发控制
同一时刻不同事务看到不同版本的数据
核心组成:
隐藏列:
trx_id: 事务ID
roll_pointer: 回滚指针
row_id: 行ID(非必须)
undo log:
记录数据的历史版本
形成版本链
read view:
活跃事务ID列表
min_trx_id、max_trx_id
查询流程:
获取read view
查看数据trx_id
根据可见性规则判断是否可见
不可见则通过undo log查找历史版本
快照读 vs 当前读:
快照读: SELECT(MVCC)
当前读: SELECT...FOR UPDATE, INSERT, UPDATE, DELETE(加锁)
104. InnoDB 事务原子性是依靠什么机制保证的?
undo log:
记录修改前的数据
事务回滚时恢复数据
工作流程:
事务执行时,先记录undo log
修改数据
如果需要回滚,根据undo log恢复
事务提交后,undo log可能用于其他事务的MVCC
undo log类型:
Insert undo log: 插入操作
Update undo log: 更新删除操作
2.3 分布式事务
105. 分布式场景下,如何解决 MySQL 事务的分布式一致性问题?
方案对比:
2PC (两阶段提交):
准备阶段
提交阶段
缺点: 协调者单点、同步阻塞
3PC (三阶段提交):
CanCommit
PreCommit
DoCommit
改善: 添加超时、预提交
TCC (Try-Confirm-Cancel):
Try: 预留资源
Confirm: 确认执行
Cancel: 取消预留
优点: 性能好
缺点: 业务侵入性大
Seata AT模式:
自动生成回滚SQL
对业务侵入小
消息事务:
本地消息表
消息队列
106. 2PC 分布式协议最大的缺陷是什么?
2PC (Two-Phase Commit):
阶段一:准备阶段
协调者向所有参与者发送Prepare请求
参与者执行事务但不提交
返回成功或失败
阶段二:提交阶段
全部成功:发送Commit
有失败:发送Rollback
最大缺陷:
同步阻塞:
所有参与者在等待期间阻塞
其他事务无法执行
协调者单点:
协调者故障会导致阻塞
可能导致数据不一致
数据不一致:
部分参与者收到Commit,部分没收到
导致数据不一致
无法解决:
协调者发出Commit后宕机
参与者无法得知最终结果
107. 2PC 的原理是什么?有什么缺点?
2PC(两阶段提交)原理:
Prepare阶段
协调者向参与者发送 prepare
参与者执行本地事务并写预提交日志,不真正提交
返回“可提交/不可提交”
Commit/Rollback阶段
若全部参与者返回可提交,协调者广播 commit
若任一失败,广播 rollback
缺点:
同步阻塞:参与者在等待决议期间锁资源
协调者单点:协调者故障会导致参与者长时间不确定状态
网络分区风险:可能出现部分提交、部分回滚
恢复复杂:异常场景下需要额外日志与人工干预
108. 3PC 相比 2PC 有什么改进?
3PC (Three-Phase Commit):
阶段:
CanCommit: 协调者询问是否可以提交
PreCommit: 协调者发送预提交,参与者执行
DoCommit: 协调者发送真正提交
改进点:
增加超时机制:
参与者等待PreCommit超时,自动提交
减少阻塞时间
两阶段拆分:
将提交过程拆分为预提交和提交
减少数据不一致风险
更好的故障恢复:
参与者可以自行判断状态
仍存在的问题:
协调者单点未完全解决
网络分区问题仍存在
性能开销大
109. TCC 模式的原理是什么?
TCC (Try-Confirm-Cancel):
三个阶段:
Try (预留):
资源预留
冻结资源
记录预操作
Confirm (确认):
确认执行
真正消耗资源
预留转为实际
Cancel (取消):
取消预留
释放资源
示例:转账:
Try: 冻结源账户金额
Confirm: 从源账户扣款,加载目标账户
Cancel: 解冻源账户金额
特点:
需要业务代码实现
性能好
侵入性大
110. Seata AT 模式是什么?
Seata AT (Automatic Transaction):
工作原理:
解析SQL: 分析SQL类型和表结构
生成回滚SQL: 自动生成反向SQL
执行业务SQL: 执行正向SQL
保存回滚日志: 保存到undo_log表
提交事务: 提交本地事务
特点:
对业务侵入小
自动生成回滚SQL
支持高并发
需要undo_log表
适用场景:
微服务分布式事务
数据一致性要求高
2.4 SQL 优化
111. 慢SQL如何定位?
开启慢查询日志:
slow_query_log = 1 long_query_time = 1 slow_query_log_file = /var/log/mysql/slow.log查看慢查询日志:
使用mysqldumpslow分析
mysqldumpslow -t 10 slow.log
EXPLAIN分析:
EXPLAIN SELECT * FROM user WHERE id = 1;SHOW PROCESSLIST:
查看当前执行的SQL
Performance Schema:
MySQL 5.6+性能分析
监控工具:
Prometheus + Grafana
MySQL Exporter
112. MySQL 慢 SQL 排查处理优化
排查步骤:
开启慢查询
EXPLAIN分析
type: 访问类型
key: 使用索引
rows: 扫描行数
Extra: 额外信息
检查索引
是否命中索引
是否覆盖索引
检查数据量
是否大表查询
检查执行计划
是否全表扫描
优化方案:
加索引
优化SQL结构
减少返回字段
分页优化
113. MySQL 执行一条 SQL 的过程?
连接器:
连接管理
权限验证
查询缓存:
命中直接返回(MySQL 8.0移除)
解析器:
词法分析
语法分析
生成AST
预处理器:
检查表、字段存在性
权限检查
优化器:
生成执行计划
选择索引
确定连接顺序
执行器:
调用存储引擎
返回结果
存储引擎:
InnoDB执行具体操作
114. 那么多索引 MySQL 怎么选?
优化器选择索引的逻辑:
统计信息:
表的cardinality
索引基数
成本分析:
IO成本
CPU成本
规则判断:
最左前缀
索引下推
强制索引:
FORCE INDEX
影响因素:
索引区分度
索引长度
统计信息准确性
查询条件
115. 如何分析一条慢 SQL?
EXPLAIN分析:
EXPLAIN FORMAT=JSON SELECT ...关键字段:
type: 访问类型(最好const-ref-range)
possible_keys: 可用索引
key: 实际使用索引
rows: 扫描行数
Extra: Using filesort/Using temporary
SHOW PROFILE:
SET profiling = 1; SELECT ...; SHOW PROFILES; SHOW PROFILE FOR QUERY 1;慢查询日志:
找到执行时间长的SQL
116. EXPLAIN 主要看哪些字段?
重点关注:
type: 最好const/ref/range,避免ALL
key: 是否使用索引
rows: 扫描行数,越少越好
Extra: 避免Using filesort/temporary
117. MySQL 深度分页优化的核心方案是什么?
问题: OFFSET很大时性能差
方案:
子查询优化:
SELECT * FROM orders WHERE id > (SELECT id FROM orders LIMIT 100000, 1) LIMIT 10;延迟关联:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders LIMIT 100000, 10) t ON o.id = t.id;游标分页:
使用上一页最后一条的ID
WHERE id > lastId LIMIT 10
分库分表:
按时间或ID分表
减少单表数据量
ES搜索:
大数据量使用ElasticSearch
118. 慢 SQL 的底层成因(除索引问题外)有哪些,如何从数据库内核层面优化?
其他原因:
锁等待:
行锁、表锁
解决:减少锁范围,优化事务
连接数:
连接数过多
解决:使用连接池
服务器资源:
CPU、IO瓶颈
解决:升级硬件
配置问题:
缓冲区大小
解决:调优参数
数据量:
单表数据量太大
解决:分库分表
内核优化:
调整buffer pool
优化redo log配置
调整thread pool
119. 你实际做过SQL优化的落地实现吗?具体怎么做的?
做过,典型案例是订单查询接口在大促时P99超过2秒。
落地步骤:
定位: 慢查询日志 + APM定位到一条多条件分页SQL
分析:
EXPLAIN发现type=ALL,并出现Using filesort优化:
新增联合索引(按过滤条件与排序字段顺序设计)
将
SELECT *改为按需字段深分页改成“游标分页”
验证: 线上影子流量回放,确认执行计划稳定命中索引
结果: P99从2s+降到200ms左右,DB CPU明显下降
固化: 增加SQL评审规范(禁止无索引大表分页)
120. 多表联合查询(比如 LEFT JOIN)变慢了,你一般会从哪些地方入手排查和优化?有实际处理过的例子吗?
排查方向:
检查索引:
关联字段是否都有索引
是否有覆盖索引
检查数据量:
是否有大表JOIN
数据分布情况
检查SQL写法:
避免笛卡尔积
筛选条件提前
检查执行计划:
是否有嵌套循环
是否需要小表驱动
优化方案:
增加索引
优化JOIN顺序
分页优化
减少返回字段
2.5 数据库锁与运维
121. 行锁和表锁的区别?
行锁:
锁定行记录
InnoDB支持
粒度细,并发高
开销大
表锁:
锁定整张表
MyISAM/InnoDB支持
粒度粗,并发低
开销小
加锁方式:
-- 行锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 表锁
LOCK TABLES user READ;
LOCK TABLES user WRITE;122. 乐观锁和悲观锁的区别?
悲观锁:
假设会发生冲突
先获取锁再操作
实现:SELECT...FOR UPDATE
适合写多场景
乐观锁:
假设不会冲突
先操作,验证冲突再重试
实现:版本号/CAS
适合读多写少场景
实现示例:
-- 悲观锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 乐观锁(版本号)
UPDATE user SET balance = balance - 10, version = version + 1
WHERE id = 1 AND version = 1;123. 给几十万条数据的表新增字段时,如何避免锁表影响业务?
方案:
在线DDL:
MySQL 5.6+支持
ALGORITHM=INSTANT
pt-online-schema-change:
Percona工具
创建新表,复制数据,替换
ghost:
GitHub工具
类似pt-osc
业务低峰期:
选择业务低峰期执行
分批处理:
小批量添加
最佳实践:
评估影响范围
提前备份
监控执行过程
124. MySQL中如何创建、查询、修改和删除表?
-- 创建表
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT
);
-- 查询表结构
DESC user;
SHOW CREATE TABLE user;
-- 修改表
ALTER TABLE user ADD COLUMN email VARCHAR(100);
ALTER TABLE user MODIFY COLUMN name VARCHAR(100);
ALTER TABLE user DROP COLUMN email;
-- 删除表
DROP TABLE user;三、Redis
3.1 数据结构与基础
126. Redis的内存淘汰机制包含哪些策略?
Redis内存淘汰策略:
noeviction (默认):
不删除,返回错误
volatile-lru:
删除设置了过期时间的键,LRU算法
allkeys-lru:
所有键,LRU算法
volatile-ttl:
删除TTL最小的键
volatile-random:
随机删除设置了过期时间的键
allkeys-random:
随机删除所有键
volatile-lfu:
删除使用频率最低的键(Redis 4.0+)
allkeys-lfu:
删除所有键中使用频率最低的(Redis 4.0+)
127. Redis 有哪些数据结构?
基本数据类型:
String: 字符串,最大512MB
List: 双向链表
Set: 无序集合
ZSet: 有序集合
Hash: 哈希表
高级数据类型:
Bitmap: 位图
HyperLogLog: 基数统计
Stream: 消息队列
Geospatial: 地理位置
128. String 类型的底层实现是什么?
RedisObject:
type: string (0)
encoding: 编码类型
ptr: 指向实际数据
编码类型:
int: 整数,用long类型存储
embstr: 短字符串,一次分配
raw: 长字符串,多次分配
embstr vs raw:
embstr: ≤44字节
raw: >44字节
129. Hash 底层是怎么实现的?
两种编码:
ziplist (压缩列表):
元素少时使用
节省内存
元素: <512个,value<64字节
hashtable (哈希表):
元素多时转换
O(1)复杂度
转换条件:
hash-max-ziplist-entries: 512
hash-max-ziplist-value: 64字节
130. Redis 的 String 类型在业务中最常用的一个场景是什么?
缓存:
缓存数据库查询结果
序列化JSON存储
分布式锁:
SETNX命令
SET key value NX EX seconds
Session存储:
用户Session信息
计数器:
INCR、DECR原子操作
限流:
滑动窗口
3.2 持久化机制
131. Redis的持久化机制有哪些?分别怎么实现?
RDB (Redis Database):
定时生成数据快照
save/bgsave命令
触发机制:时间/命令/配置
AOF (Append Only File):
记录所有写命令
appendonly yes开启
三种刷盘策略
混合持久化:
Redis 4.0+
RDB+AOF结合
132. RDB(快照持久化)
实现方式:
save: 阻塞主线程
bgsave: fork子进程
触发机制:
手动: save/bgsave
自动: 配置save m n
优点:
文件紧凑,适合备份
恢复快
缺点:
可能有数据丢失
fork子进程开销大
133. AOF(追加日志持久化)
三种刷盘策略:
always: 每次写都刷盘
everysec: 每秒刷盘(默认)
no: 操作系统决定
重写机制:
压缩AOF文件
bgrewriteaof命令
优点:
数据丢失少
可读性好
缺点:
文件比RDB大
恢复慢
134. RDB 和 AOF 的区别?
建议:
同时开启
混合持久化
135. 混合持久化是什么意思?
Redis 4.0+支持:
AOF重写时,使用RDB格式写入开头
后续命令用AOF格式追加
恢复时先加载RDB部分
优势:
结合RDB和AOF优点
恢复更快
数据更完整
3.3 缓存问题
136. 缓存穿透是什么?如何解决?
定义:
查询一个不存在的数据
数据库查不到,缓存也没有
大量请求直接打到数据库
解决方案:
布隆过滤器:
判断key是否存在
存在才查缓存和DB
空值缓存:
查询结果为空也缓存
设置较短过期时间
参数校验:
过滤非法请求
限流:
防止恶意攻击
137. 缓存击穿是什么?如何解决?
定义:
热点key过期瞬间
大量请求同时穿透到数据库
解决方案:
互斥锁:
只允许一个请求查DB
其他等待缓存结果
逻辑过期:
不设置过期时间
用后台异步刷新
永不过期:
热点数据不设过期
138. 缓存雪崩是什么?如何解决?
定义:
大量缓存同时失效
请求全部打到数据库
解决方案:
随机过期时间:
过期时间加随机值
多级缓存:
本地缓存 + Redis
高可用:
Redis集群
限流降级:
保护数据库
预热:
提前加载数据
3.4 分布式锁
139. 分布式锁的可重入性如何实现
可重入锁:
同一线程可以多次获取
内部维护计数器
Redis实现:
value存储线程标识+计数器
判断是否是当前线程
Lua脚本保证原子性
示例:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('incr', KEYS[1])
else
return redis.call('set', KEYS[1], 1)
end140. 分布式锁原理大概是什么?
核心思想:
多个客户端竞争同一把锁
只有获取到锁的才能执行
SET命令:
SET lock_key unique_value NX PX 30000NX: 不存在才设置
PX: 过期时间
释放锁:
判断value后删除
用Lua脚本保证原子性
141. SETNX 的功能是什么?
SETNX (SET if Not eXists):
当key不存在时设置值
当key存在时不做操作
返回1表示成功,0表示失败
用途:
分布式锁
防止重复
142. 加锁后进程异常退出,锁泄露怎么办?
问题:
获取锁后进程崩溃
锁未释放,其他进程无法获取
解决方案:
设置过期时间:
PX参数设置过期时间
即使未主动释放,过期自动释放
Watchdog:
Redisson实现
自动续期
死锁检测:
监控锁状态
定期清理
143. Redisson 分布式锁的实现原理?
核心机制:
SETNX + Lua脚本:
获取锁
释放锁
Watchdog (看门狗):
自动续期
默认10秒检查一次
延长锁过期时间
可重入:
维护重入计数
公平锁:
等待队列
RedLock:
多个Redis实例
144. Redis 连接池的作用?
复用连接:
避免频繁创建销毁连接
提高性能:
减少TCP握手开销
资源控制:
控制最大连接数
故障恢复:
连接异常自动重连
145. 分布式锁相关:set nx ex、watch dog、redlock
set nx ex:
原子性加锁
带过期时间
Watchdog:
自动续期机制
Redisson实现
RedLock:
多个Redis节点
多数节点成功才算获取成功
提高可靠性
146. 如何设计一个分布式锁?需要考虑哪些问题?
需要考虑:
互斥性:
同一时间只能一个客户端获取
死锁:
客户端异常,锁无法释放
解决方案:过期时间
可重入:
同一线程可多次获取
高性能:
获取/释放锁快
高可用:
Redis集群部署
公平性:
按请求顺序获取锁
147. 先执行set nx再执行set ex会存在什么问题
问题:
两步操作不原子
中间可能崩溃
导致锁永不过期
正确做法:
SET lock_key value NX PX 30000或使用Lua脚本:
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('pexpire', KEYS[1], ARGV[2])
return 1
else
return 0
end3.5 集群与高可用
148. Redis 主从复制原理?
流程:
从节点连接主节点
从节点发送SYNC/PSYNC
主节点fork子进程
子进程发送RDB
后续发送增量命令
复制类型:
全量复制
增量复制
原理:
主节点写操作记录到repl_backlog
从节点同步
149. 哨兵模式的作用?
作用:
监控:
监控主从节点健康状态
自动故障转移:
主节点下线后
选举从节点为新主节点
通知:
客户端通知
核心功能:
Sentinel本身是高可用的
多个Sentinel组成集群
150. Redis Cluster 的原理?
槽位分配:
16384个槽
每个节点负责一部分
数据路由:
计算key的槽位
跳转到对应节点
故障转移:
节点间PING/PONG
故障节点下线后重新分配
特点:
去中心化
数据分片
高可用
151. Redis 哨兵和 Cluster 集群,你觉得它们核心区别在哪?平时项目里怎么选?(比如数据量大小、高可用要求)
哨兵模式:
主从复制
一个主节点,多个从节点
数据保存在主节点
适合数据量较小
Cluster集群:
数据分片
多个主节点
数据分布在不同节点
适合大数据量
选择建议:
< 10万QPS: 哨兵
10万QPS: Cluster
3.6 实际应用
152. 缓存预热:Redis预减库存(DECR stock + Lua脚本原子操作)
预减库存方案:
-- Lua脚本
local stock = redis.call('GET', KEYS[1])
if stock == false or tonumber(stock) < tonumber(ARGV[1]) then
return -1
end
return redis.call('DECRBY', KEYS[1], ARGV[1])优势:
原子操作
避免超卖
高并发性能好
153. Redis 怎么保证多扣/多增的可靠性?
方案:
Lua脚本:
原子操作
事务:
MULTI/EXEC
乐观锁:
WATCH
限流:
控制并发
154. Redis 为什么这么快?
纯内存操作:
内存访问纳秒级
单线程:
避免上下文切换
无锁开销
IO多路复用:
epoll机制
高并发处理
高效数据结构:
简单动态字符串
压缩列表
跳跃表
协议简单:
RESP协议
155. Redis 淘汰策略?
当Redis内存达到 maxmemory 后,会按配置策略淘汰key:
noeviction:不淘汰,写请求报错(默认)
allkeys-lru:从所有key中淘汰最近最少使用
allkeys-lfu:从所有key中淘汰访问频率最低
allkeys-random:随机淘汰所有key
volatile-lru:仅在设置过期时间的key里做LRU淘汰
volatile-lfu:仅在过期key里做LFU淘汰
volatile-random:仅在过期key里随机淘汰
volatile-ttl:优先淘汰TTL更小(更快过期)的key
选型建议:
通用缓存优先
allkeys-lru或allkeys-lfu若必须保留持久key,可考虑
volatile-*
156. Redis 过期 key 删除策略?
三种策略:
惰性删除:
访问时检查
CPU友好,内存不友好
定期删除:
定时检查
平衡方案
定时删除:
设置过期时间同时删除
CPU不友好
Redis实现:
惰性删除 + 定期删除
四、计算机网络
4.1 TCP/UDP
157. TCP是如何保证可靠传输的?
校验和:
校验数据完整性
序列号:
标识数据顺序
确认机制(ACK):
确认收到数据
重传机制:
超时重传
快速重传
流量控制:
滑动窗口
避免发送过快
拥塞控制:
慢启动
拥塞避免
快重传
快恢复
158. TCP 与 UDP 的区别?
159. TCP 为什么是可靠的?
TCP可靠性来自“检测 + 纠错 + 流控 + 拥塞控制”的组合机制:
序列号机制:保证数据有序重组
ACK确认:发送方确认接收方是否收到
超时重传:超时未确认则重传丢失报文
快速重传:收到重复ACK可提前重传
校验和:检测数据损坏
滑动窗口:按接收能力控制发送速率
拥塞控制:根据网络状态动态调整发送窗口
因此TCP不是“永不丢包”,而是“即使丢包也能恢复并保证应用层看到可靠字节流”。
160. 三次握手的过程?
流程:
客户端 服务端
| |
|------ SYN=1, seq=x ------>|
| |
|<--- SYN=1, ACK=1, seq=y, ack=x+1 ----|
| |
|------ ACK=1, seq=x+1, ack=y+1 ----->|
| |
(连接建立)目的:
确认双方接收能力
同步序列号
161. TCP 三次握手和四次挥手为什么是这样设计的?
三次握手原因:
确认双方序列号
避免历史连接
确保连接建立
四次挥手原因:
双向连接需要双向关闭
确认数据发送完成
每方都需要FIN和ACK
162. 四次挥手的过程?
客户端 服务端
| |
|------ FIN=1, seq=u ------>|
| |
|<--- ACK=1, ack=u+1 ----|
| |
| (客户端到服务端关闭)
| |
|<--- FIN=1, seq=v ----|
| |
|------ ACK=1, ack=v+1 ----->|
| |
(连接关闭)TIME_WAIT:
等待2MSL
确保最后ACK到达
163. TCP 拥塞控制流程是什么?
四个算法:
慢启动:
cwnd从1开始
指数增长
拥塞避免:
cwnd达到阈值后
线性增长
快重传:
连续3个重复ACK
立即重传
快恢复:
cwnd减半
拥塞避免
状态:
慢开始
拥塞避免
超时:cwnd=1
快恢复:cwnd=threshold/2
4.2 HTTP/HTTPS
164. HTTP协议的工作原理是什么?常见状态码有哪些?
HTTP工作原理:
客户端建立TCP连接
发送HTTP请求
服务器处理请求
返回HTTP响应
关闭连接
状态码:
1xx: 信息响应
2xx: 成功(200, 201, 204)
3xx: 重定向(301, 302, 304)
4xx: 客户端错误(400, 401, 403, 404)
5xx: 服务端错误(500, 502, 503)
165. HTTP1.0、HTTP1.1、HTTP2.0 的区别?
166. HTTP 和 HTTPS 的区别?
167. HTTPS的握手过程是怎样的?为什么需要三个随机数?
握手过程:
客户端发送ClientHello
服务端发送ServerHello + 证书
客户端验证证书 + 发送预主密钥
服务端确认
生成会话密钥
三个随机数:
client_random
server_random
pre_master_secret
原因:
增强随机性
防止重放攻击
提高安全性
168. HTTPS 的原理?
HTTPS = HTTP + TLS/SSL。
核心原理:
身份认证:通过CA证书证明服务端身份
密钥协商:握手阶段协商会话密钥
对称加密传输:业务数据用会话密钥加密,性能高
完整性校验:防止传输中被篡改
握手关键步骤(简化):
ClientHello(算法套件、随机数)
ServerHello + 证书
客户端校验证书并协商密钥
双方生成会话密钥
进入加密通信
169. 请介绍一下HTTP和HTTPS的区别,以及HTTPS的握手过程
HTTP vs HTTPS:
HTTP明文传输,HTTPS加密传输
HTTP默认80端口,HTTPS默认443端口
HTTPS需要证书,HTTP不需要
HTTPS可提供身份认证、机密性、完整性
HTTPS握手过程(面试版):
客户端发起
ClientHello服务端返回
ServerHello和证书客户端验证证书合法性(CA链、域名、有效期)
双方协商出会话密钥
后续HTTP数据通过会话密钥加密传输
170. HTTP3 和 QUIC 了解吗?
HTTP3:
基于QUIC协议
UDP实现
0-RTT连接
无队头阻塞
连接迁移
QUIC:
Google提出
混合TCP+UDP优点
TLS内置
连接ID
171. 说说HTTP和HTTPS的区别,HTTPS的性能开销在哪里?
性能开销:
SSL/TLS握手:
额外的RTT
密钥交换计算
加密/解密:
CPU计算资源
对称加密影响小
证书验证:
证书链验证
优化:
证书预加载
Session复用
HTTP/2
OCSP stapling
172. 什么是中间人攻击?HTTPS如何防御?
中间人攻击:
攻击者插入通信双方
窃听/篡改数据
HTTPS防御:
证书验证:
验证证书链
验证域名
检查有效期
公钥基础设施:
CA信任链
证书透明度:
CT日志
TLS加密:
数据加密传输
4.3 DNS与URL
173. URL请求过程中,DNS解析的核心作用是什么?
DNS作用:
将域名解析为IP地址
分布式数据库
分层查询
解析过程:
浏览器缓存
本地DNS缓存
Hosts文件
DNS服务器递归查询
174. DNS解析只有浏览器才支持吗?详细说说DNS解析过程
解析流程:
浏览器缓存: 检查缓存
系统缓存: OS hosts文件
本地区域服务器: ISP DNS
根域名服务器: .
顶级域名服务器: .com
权威域名服务器: example.com
解析方式:
递归查询
迭代查询
4.4 网络编程
175. Socket 编程了解吗?
Socket概念:
网络通信的抽象
端到端的通信
基本流程:
服务端:
socket() 创建socket
bind() 绑定地址
listen() 监听
accept() 接受连接
read()/write() 通信
close() 关闭
客户端:
socket() 创建socket
connect() 连接
write()/read() 通信
close() 关闭
176. 长连接和短连接的区别?
短连接:
每次请求都建立连接
请求完成后关闭
适合请求少
长连接:
保持连接复用
适合高并发
需要心跳保活
HTTP中的区别:
HTTP/1.0短连接
HTTP/1.1默认长连接
177. 网络编程中的select、poll、epoll有什么区别?
epoll优势:
适合大量连接
效率高
无需遍历
178. Epoll的底层实现原理,为什么比select/poll快?ET和LT模式有什么区别?
epoll原理:
监听红黑树
就绪链表
回调机制
为什么快:
不遍历所有fd
无频繁用户/内核切换
直接返回就绪的fd
ET vs LT:
LT (Level Trigger):
水平触发
持续通知
简单,不易丢数据
ET (Edge Trigger):
边沿触发
只通知一次
效率高,需要处理完
五、消息队列
5.1 消息队列作用
179. 消息队列的作用是什么?
异步处理:
提高系统响应速度
削峰填谷:
缓解高并发压力
解耦:
上下游系统松耦合
消息通讯:
点对点、发布订阅
180. 为什么使用消息队列?
使用消息队列的核心原因有四点:
异步解耦
请求链路拆短,主流程先返回,非核心逻辑异步处理
削峰填谷
高峰请求先入队,消费者按稳定速率处理,保护下游DB
系统解耦与扩展
生产者和消费者独立演进,新增下游只需新增消费者
最终一致性与可靠交付
配合重试、死信、补偿机制,提升分布式场景下可靠性
注意:
MQ会引入复杂度(幂等、顺序、延迟、可观测),要按场景使用
181. 削峰填谷是什么意思?
削峰:
高并发时,消息积压在队列
数据库压力减少
填谷:
高峰过后,消息逐步处理
系统平稳运行
5.2 Kafka
182. Kafka 分区的目的是什么?压力具体指什么?
分区目的:
并行消费:
多分区多消费者
负载均衡:
数据分布到多个分区
提高吞吐:
水平扩展
压力:
磁盘IO
网络带宽
内存使用
183. Kafka 怎么保证消息有序性?
单分区内有序:
同一分区内有序
全局有序:
只用一个分区
牺牲并发
分区规则:
同一业务数据发送到同一分区
184. 消费者组的原理?
消费者组:
多个消费者组成
共同消费主题
分区数=消费者数(最理想)
特点:
同一组内不会重复消费
不同组可以重复消费
185. Kafka 和 RocketMQ 的区别?
5.3 RocketMQ
186. 为什么选用 RocketMQ 而不是 Kafka?
业务场景:
事务消息
顺序消息
延迟消息
可靠性要求:
金融级场景
消息不丢失
功能丰富:
死信队列
消息重试
187. 延迟队列是怎么实现的?
RocketMQ:
延迟等级
messageDelayLevel配置
消息投递到SCHEDULE_TOPIC_XXXX
实现原理:
按延迟等级存储
定时器检查投递
188. 事务消息的原理?
两阶段提交:
发送半消息:
先发送到MQ
标记为prepared状态
执行本地事务:
业务操作
提交/回滚:
成功:提交消息
失败:回滚消息
补偿机制:
定时检查未决事务
189. RabbitMQ 的镜像队列机制是如何保证消息高可用的?
镜像队列:
主从复制
消息同步到多个节点
机制:
master节点处理读写
slave同步
master失效,slave升级
模式:
classic
quorum(新版)
5.4 消息可靠性
190. 消息重复消费怎么办?
原因:
网络抖动
消费端未ACK
生产者重试
解决方案:
幂等性:
唯一ID
去重表
业务层处理:
状态机
乐观锁
191. 消息丢失怎么办?
丢失环节:
生产端丢失
MQ存储丢失
消费端丢失
解决方案:
生产端:
确认机制
重试
MQ端:
持久化
多副本
消费端:
手动ACK
幂等处理
192. 项目中使用RabbitMQ时,如何解决消息丢失和重复消费问题?
完整链路方案:
生产端防丢:
开启 publisher confirm
发送失败重试 + 本地消息表兜底
Broker防丢:
队列/交换机持久化
镜像/仲裁队列保证高可用
消费端防丢:
业务处理成功后再ACK(手动ACK)
消费失败N次后进入死信队列
防重复消费:
消息唯一ID + 幂等表/唯一索引
业务状态机防止重复执行副作用
193. RabbitMQ 中如何保证消费端不出现消息重复消费的问题?
RabbitMQ本身无法承诺“绝不重复”,工程上要做“消费幂等”。
常见做法:
每条消息携带全局唯一
messageId/bizNo消费前先查幂等记录(或利用唯一索引原子判重)
已处理则直接ACK并返回
未处理则执行业务,成功后落幂等记录并ACK
注意:
幂等记录与业务更新最好在同一事务内提交
避免“业务成功但幂等记录失败”导致重复执行
194. 在项目中,使用消息队列有没有遇到什么问题?
遇到过,主要是三类:
消息积压
原因:消费能力低于生产速度
处理:增加消费者并发、按业务分队列、高峰期降级非核心消息
消费失败重试风暴
原因:下游依赖异常导致大量重试
处理:引入指数退避、死信队列、失败熔断
顺序错乱
原因:并发消费导致同一业务Key乱序
处理:同Key路由同队列,消费端串行化处理
六、JVM 虚拟机
6.1 内存模型
195. 简述 JVM 有哪些内存结构
程序计数器: 线程私有,记录字节码行号
虚拟机栈: 线程私有,方法栈帧
本地方法栈: 线程私有,native方法
堆: 线程共享,对象实例
方法区/元空间: 线程共享,类信息
196. JVM 内存模型是怎样的?
这个问题要区分两层含义:
JVM运行时数据区(内存结构)
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆、方法区(元空间)
JMM(Java Memory Model)并发内存模型
规定线程如何通过主内存与工作内存交互
核心是保证可见性、有序性、原子性语义
volatile/synchronized/Lock都是在JMM规则下生效
面试建议:
如果面试官问“内存模型”,先反问是“运行时数据区”还是“JMM并发模型”,体现专业性。
197. 堆和栈的区别?
198. 方法区存放什么数据?
类信息:
类的元数据
成员信息
静态变量:
static修饰的变量
常量:
字符串常量池
JIT代码:
即时编译器生成的代码
199. 哪些区域比较容易发生 OOM
堆OOM:
对象太多
内存泄漏
栈OOM:
递归太深
线程太多
元空间OOM:
类太多
动态生成类
直接内存OOM:
NIO使用过多
201. 对象创建过程?
类加载检查:
检查符号引用
分配内存:
指针碰撞/空闲列表
初始化零值:
默认值
设置对象头:
Mark Word
元数据信息
执行init:
构造函数
202. 对象如何判断可以回收?
算法:
引用计数:
引用为0可回收
循环引用问题
可达性分析:
GC Roots向下搜索
不在引用链可回收
GC Roots:
栈引用
静态引用
常量引用
本地方法引用
203. 垃圾回收算法有哪些?
标记-清除:
标记-清除
效率低、碎片
复制:
分为两块
简单高效
内存减半
标记-整理:
标记-整理
无碎片
效率中等
分代收集:
新生代复制
老年代整理
204. Full GC 触发机制?
老年代空间不足
System.gc()
元空间不足
Minor GC前检查
CMS并发失败
205. 内存分配策略?
对象优先Eden:
新对象在Eden
大对象直接进老年代:
大于阈值
长期存活对象进老年代:
15次Minor GC
动态年龄判断:
Survivor相同年龄超过一半
206. JVM 内存分配与回收过程?
Eden分配:
新对象在Eden
Minor GC:
Eden满触发
存活进Survivor
对象晋升:
达到年龄进老年代
Full GC:
老年代满触发
207. CMS和G1的区别
208. G1是如何实现可预测停顿时间的功能的?
G1原理:
Region划分:
将堆划分为多个Region
优先列表:
优先回收垃圾最多的Region
停顿时间目标:
MaxGCPauseMillis参数
Mixed GC:
回收年轻代+部分老年代
209. G1 垃圾回收器中 Remembered Set 的作用是什么?
Remembered Set:
记录Region间的引用关系
避免全堆扫描
作用:
年轻代引用老年代:记录
老年代引用年轻代:记录
实现:
每个Region有一个RSet
记录其他Region对它的引用
210. ZGC 颜色指针的核心解决了什么问题?
颜色指针:
指针本身存储GC状态
42位对象指针
解决的问题:
并发标记:
无需STW
对象移动:
无需更新引用
读屏障:
并发转移
可扩展性:
支持TB级堆
6.2 类加载
211. 类加载机制?
加载:
读取字节码
验证:
字节码验证
准备:
分配内存
解析:
符号引用变直接引用
初始化:
静态代码块
212. 双亲委派模型?
加载器:
Bootstrap ClassLoader
Extension ClassLoader
Application ClassLoader
委派流程:
向上委派给父加载器
父加载不了才自己加载
优点:
避免类重复加载
安全性(防止篡改)
七、分布式系统与微服务
7.1 分布式理论
213. CAP 定理是什么?
CAP:
Consistency (一致性): 所有节点数据一致
Availability (可用性): 每次请求都能获得响应
Partition Tolerance (分区容错): 系统在网络分区下仍能运行
三者不可兼得:
CA: 单点系统
CP: 优先一致性
AP: 优先可用性
214. BASE 理论是什么?
BASE:
Basically Available: 基本可用
Soft State: 软状态
Eventually Consistent: 最终一致性
是对CAP的补充:
放弃强一致性
保证最终一致性
7.2 分布式ID
215. 分布式 ID 生成器怎么实现?
UUID:
无序
性能好
数据库自增:
有序
单点
雪花算法:
有序
高性能
Redis INCR:
有序
依赖Redis
216. Snowflake 算法的原理?
结构:
1位符号位
41位时间戳
10位机器ID
12位序列号
特点:
有序
高性能
依赖时钟
7.3 微服务架构
217. 说说你对微服务架构的理解,以及遇到的问题和解决方案
微服务:
服务拆分
独立部署
独立数据库
问题:
服务治理
分布式事务
服务通信
解决方案:
Spring Cloud
Dubbo
218. 微服务架构的优缺点?
优点:
独立部署
技术异构
弹性伸缩
快速迭代
缺点:
复杂度高
分布式事务
运维难度
服务治理
219. 服务注册与发现?
注册中心:
Eureka
Nacos
Consul
流程:
服务启动注册
心跳保活
服务发现
负载均衡
220. 配置中心的作用?
作用:
统一配置管理
动态配置更新
环境隔离
版本管理
常见:
Nacos
Apollo
Spring Cloud Config
221. API 网关的作用?
路由转发:
请求路由
认证鉴权:
身份验证
限流熔断:
保护服务
协议转换:
HTTP/Dubbo
日志记录:
访问日志
7.4 负载均衡
222. 负载均衡中,轮询算法的核心缺陷是什么?
轮询缺陷:
不考虑服务器性能
不考虑当前负载
不考虑网络状况
改进算法:
加权轮询
最少连接
IP哈希
7.5 高可用
223. 你们的系统是如何保证高可用的?如果服务器宕机了怎么办?
高可用方案:
负载均衡
服务冗余
健康检查
自动故障转移
限流熔断
宕机处理:
流量切换
告警通知
224. 线上转码服务挂了,你们有什么后续处理?
(根据项目回答)
告警通知
流量切换
问题定位
服务恢复
复盘分析
十、AI / 大模型
10.1 LLM 交互
235. 大模型调用数据库的全链路流程?
用户请求: 用户输入问题
意图识别: 判断是否需要查询数据库
SQL生成: LLM生成SQL
SQL校验: 验证SQL安全性
执行查询: 连接数据库执行
结果处理: 格式化结果
生成回答: 结合数据生成最终回复
236. 什么是 Function Calling?
Function Calling:
大模型调用外部函数的能力
让AI能执行具体操作
扩展AI的能力边界
流程:
定义函数签名
用户请求
判断是否调用函数
执行函数获取结果
将结果返回给模型
生成最终回复
237. MCP 是什么?
MCP (Model Context Protocol):
Anthropic提出的模型上下文协议
标准化的AI与外部工具交互方式
统一接口访问各种资源
核心:
工具调用
资源访问
提示词管理
238. Skills 是什么?
Skills:
预定义的工具集
封装好的函数调用
解决特定问题
用途:
文件操作
代码执行
命令运行
239. skills和tools和mcp之间的区别
10.2 RAG
240. RAG 和传统搜索有什么区别?
241. 为什么不直接用关键词检索?
同义词问题:
用户说"电脑"希望找到"计算机"
语义理解:
理解真实意图
歧义消除:
"苹果"可能是水果或公司
242. RAG 效果优化有哪些方法?
文档分块:
合理分块大小
向量化模型:
选择更好的模型
混合检索:
关键词+向量
重排序:
ReRanker优化
查询改写:
HyDE等
243. 利用PGVector的混合索引(稠密+稀疏)提升召回率;
实现:
稠密向量: 语义相似度
稀疏向量: BM25关键词
结合两者优势
244. 使用HyDE算法,先让大模型生成初步答案再转为向量进行检索;
HyDE:
让LLM生成假设答案
将假设答案向量化
用假设答案检索
结合真实问题检索
245. 引入ReRanker对召回结果重排序,以平衡召回率与精确度。
ReRanker:
对初检结果重排序
更精准的模型
平衡精确率和召回率
10.3 Agent
246. 智能体和大模型怎么交互、协同?工作流程是什么?
工作流程:
用户输入
理解意图
规划步骤
执行工具
评估结果
生成回复
交互方式:
工具调用
状态传递
循环迭代
247. Agent 间通信和状态管理怎么设计?
通信方式:
共享状态:
共享内存/数据库
消息队列:
异步通信
API调用:
同步通信
状态管理:
持久化存储
上下文传递
248. Agent 效果评估怎么做?
自动化指标:
准确率
召回率
人工评估:
质量评分
对比测试:
A/B测试
真实场景:
生产环境监控
10.4 AI工具相关
249-257题:AI工具相关
(根据实际情况回答)
十一、算法题
11.1 排序算法
258. 写一段堆排序,不要用系统 API,15 分钟写可运行代码
public class HeapSort {
public void sort(int[] arr) {
int n = arr.length;
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 排序
for (int i = n - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, i, 0);
}
}
private void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}259. 手写快排
public class QuickSort {
public void sort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
sort(arr, low, pi - 1);
sort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}260. 手写归并排序
public class MergeSort {
public void sort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
sort(arr, left, mid);
sort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
System.arraycopy(temp, 0, arr, left, temp.length);
}
}11.2 树
261. 算法题:扁平数组化成树(二叉树)
class Node {
int val;
int parentId;
// children...
}
public List<Node> buildTree(List<Node> nodes) {
Map<Integer, Node> map = new HashMap<>();
List<Node> roots = new ArrayList<>();
for (Node node : nodes) {
map.put(node.id, node);
}
for (Node node : nodes) {
if (node.parentId == 0) {
roots.add(node);
} else {
Node parent = map.get(node.parentId);
if (parent != null) {
parent.children.add(node);
}
}
}
return roots;
}262. 算法题:层次遍历二叉树变体
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}11.3 链表
263. 反转链表
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}264. 反转双向链表
public Node reverse(Node head) {
Node curr = head;
Node prev = null;
while (curr != null) {
Node next = curr.next;
curr.next = prev;
curr.prev = next;
prev = curr;
curr = next;
}
return prev;
}11.4 设计题
265. 场景题:弱网环境下上传 1G 视频,应该怎么设计?
分片上传:
将视频分片
每片单独上传
断点续传:
记录已上传分片
失败后继续
压缩:
视频压缩
重试机制:
失败重试
秒传:
MD5校验
266. 场景题:100个进程,5个并发数的工作,应该怎么设计?
这个问题本质是“限并发执行 + 任务可观测 + 失败可恢复”。
设计目标:
同时最多执行5个任务
100个任务全部执行完成(成功/失败可统计)
单任务失败不影响整体
可重试、可超时、可监控
推荐实现(Java):
使用
ThreadPoolExecutor,核心/最大线程数都设为5使用有界队列(防止内存无限增长)
每个任务加超时控制(
Future.get(timeout))结果统一汇总(成功数、失败数、重试数)
关键参数建议:
corePoolSize = 5
maximumPoolSize = 5(严格限并发)
workQueue =
LinkedBlockingQueue(容量可控)rejectedPolicy =
CallerRunsPolicy(反压)
工程增强点:
任务幂等(避免重试导致副作用)
失败重试(指数退避)
全链路日志(traceId)
指标上报(任务耗时、成功率)
267. 算法题(全英文)给定一个二维数组:...
原题干信息不完整,但从描述可推断为“二维网格路径计数”类问题。
通用解题思路(面试可直接说):
明确状态定义:
dp[i][j][k]表示从起点到(i,j)恰好经过k个点的路径数转移方程:由上、下、左、右(或题目允许方向)转移
边界条件:起点在
k=1时路径数为1复杂度评估:时间
O(R*C*K*D),空间可滚动优化
若是经典“从A到B只能向右/向下”的版本:
状态:
dp[i][j] = dp[i-1][j] + dp[i][j-1]若要求“经过C个点”,在状态中再加一维记录步数/点数
答题建议:
先确认“可移动方向、障碍物、是否允许重复经过点、点数定义(含起终点)”
再给状态转移,最后给代码
十二、项目经验
268-286题
这组题高频考察“你是否真的做过项目”。建议统一按 STAR(背景-任务-行动-结果)回答。
通用高分回答骨架:
背景(S): 业务规模、峰值流量、核心痛点
职责(T): 你负责的模块和边界
行动(A): 技术方案、关键决策、踩坑与取舍
结果(R): 用指标量化(QPS、RT、错误率、成本)
可直接套用模板:
项目是一个
XX业务平台,高峰QPS约X万,当时核心问题是延迟高/不稳定/一致性。我负责
订单与库存链路(或检索与推荐链路),重点做了三件事:数据层:索引优化 + 慢SQL治理
缓存层:热点缓存 + 失效策略重构
异步层:MQ削峰 + 幂等消费
最终指标:P99由
Xms降到Yms,错误率由A%降到B%,机器成本下降C%。
268. 项目架构介绍?
分层:网关层 -> 业务服务层 -> 数据层 -> 基础设施层
核心组件:
Nginx/Spring Boot/MySQL/Redis/RocketMQ/ES非功能:监控、告警、限流、熔断、灰度、审计
269. 负责部分和业务接入成本?
负责模块要说清“输入/输出/依赖/边界”
接入成本用“开发天数 + 配置项 + 联调项 + 风险项”量化
270. 代码库检索?
关键词检索(symbol)+ 调用链追踪 + 提交历史
大仓库建议结合
grep + IDE references + blame
271. 你的项目使用了什么架构?包含哪些模块?数据库怎么用?
架构:微服务 + 事件驱动
模块:用户、订单、库存、支付、风控、报表
数据库:OLTP 用 MySQL,缓存用 Redis,检索用 ES
272. 介绍一下你的RPC项目?实际应用还是练手?
若实际应用:强调压测数据、故障演练、线上指标
若练手项目:强调协议设计、序列化、服务发现、容错机制
273. 项目来源(博主/学校)如何回答?
实话实说,重点转到“你做了哪些增强和原创优化”
面试官看重的是“理解深度与改造能力”,不是项目来源
281. 日志系统怎么设计,如何处理海量日志?
采集(Filebeat/Fluentd)-> 消息队列 -> 存储(ES/HDFS)-> 查询(Kibana)
分级日志、采样策略、冷热分层、索引生命周期管理(ILM)
282. 抽奖结果保存的核心技术方案?
预扣库存(Redis+Lua)-> 异步落库(MQ)-> 幂等落库(唯一键)-> 补偿对账
283. Excel转PDF不清晰如何解决?
统一字体和渲染引擎
调整DPI与页面缩放
大表分页渲染,避免一次性内存峰值
284. 权限控制模块如何设计数据权限与功能权限隔离?
功能权限:接口/菜单级别(RBAC)
数据权限:行级过滤(部门、租户、数据范围)
网关鉴权 + 服务内二次鉴权
285. 自定义异常如何保证定位准确?
统一异常码体系(系统码+业务码)
记录请求上下文(traceId、用户、参数摘要)
错误信息可观测且可追踪到代码位点
286. catch 父异常还是子异常?
一定“先子类后父类”,否则子类分支不可达
最后一个
Exception兜底,并上报监控
十三、场景题
289-300题
这组题核心是“架构能力 + 取舍能力 + 风险意识”。
289. 秒杀系统设计?
流量入口:CDN + 限流(令牌桶)+ 验签 + 防重放
核心链路:库存预扣(Redis+Lua)-> 下单异步化(MQ)-> 最终落库
防超卖:库存扣减原子化 + 幂等订单 + 失败补偿
稳定性:降级开关、熔断、隔离舱、热点隔离
290. 怎么保证积分可靠性,防止多扣/多增?
账户流水表 + 唯一业务单号 + 幂等写入
账户余额更新采用乐观锁版本号或悲观锁
引入对账任务,发现差异自动补偿
291. 高并发下数据库读写瓶颈怎么解?
读:缓存前置、读写分离、热点数据本地缓存
写:异步化、批量写、分库分表
查询:索引优化、SQL重写、避免深分页
292. 分布式存储方案?
对象存储(S3/OSS)用于文件
KV存储用于元数据热点
OLAP/ES用于分析与检索
293. 分库分表策略?
按用户ID或时间维度分片
避免跨分片事务和跨分片排序
提前规划扩容与数据迁移方案
294. 不用MQ如何实现延迟任务?
时间轮(Hashed Wheel Timer)
Redis ZSet(score=执行时间)+ 轮询抢占
数据库延迟表 + 定时扫描(低吞吐场景)
295-296. 检索效果优化案例?
召回:关键词+向量混合检索
排序:业务特征 + ReRank
评估:离线指标(NDCG/Recall)+ 在线A/B
297. 微信红包系统怎么设计?
发红包:余额冻结 -> 红包拆分 -> 入账队列
抢红包:原子扣减 + 防重复领取
资金安全:强一致账本 + 对账补偿 + 风控规则
298. RPC序列化模块优先考虑什么?
性能、兼容性、可扩展性、安全性
优先支持 schema 演进(向前/向后兼容)
299. docker代码沙箱流程?
收代码 -> 构建镜像/复用镜像 -> 启动容器 -> 执行 -> 收集结果 -> 销毁
300. 代码沙箱如何防恶意攻击?
资源限制:CPU/内存/磁盘/进程数
安全限制:seccomp、只读文件系统、禁网
时间限制:执行超时强杀
301. 判题高并发如何避免容器抖动?
容器池预热 + 任务队列 + 分级调度
冷热镜像分层缓存,减少拉镜像开销
节点资源水位控制,避免雪崩
十四、Spring / 框架
14.1 Spring
302. Spring 的核心特性?
IoC/DI:
控制反转
依赖注入
AOP:
面向切面编程
事务管理:
声明式事务
MVC:
Web框架
303. Spring Bean 的生命周期?
实例化:
new创建
属性填充:
依赖注入
初始化:
@PostConstruct
InitializingBean
销毁:
@PreDestroy
DisposableBean
304. Spring 循环依赖如何解决?
三级缓存:
singletonObjects
earlySingletonObjects
singletonFactories
解决方式:
构造器循环依赖无法解决
setter/字段循环依赖可以解决(三级缓存)
305. Spring 中 @Transactional 注解的 propagation 属性,最常用的传播行为是什么?
常用传播行为:
REQUIRED (默认): 有事务加入,无事务创建
REQUIRES_NEW: 总是创建新事务
SUPPORTS: 有事务加入,无则非事务
14.2 Spring Boot
306. Spring Boot 自动配置原理?
@SpringBootApplication:
@EnableAutoConfiguration
@EnableAutoConfiguration:
导入AutoConfigurationImportSelector
META-INF/spring.factories:
配置类路径
@Conditional:
条件装配
307. 服务端处理请求时,Tomcat的Connector组件核心职责是什么?
Connector职责:
监听端口
接收请求
协议解析
交给Container处理
14.3 MyBatis
308. MyBatis #{} 和 ${} 的区别?
309. MyBatis 一级缓存和二级缓存?
一级缓存:
SqlSession级别
默认开启
同一会话内有效
二级缓存:
SqlSessionFactory级别
跨会话有效
需要手动开启
310. MyBatis 插件原理?
原理:
拦截器链
JDK动态代理 -四大对象:
Executor
StatementHandler
ParameterHandler
ResultSetHandler
311. MyBatis 一级缓存的作用域是哪里?默认是否开启?
作用域: SqlSession
默认: 开启
14.4 其他框架
312. 基于 AOP 思想,如何自定义拦截器防止 SQL 注入?
定义注解
编写切面
拦截参数
过滤特殊字符
十五、设计模式
490-495题
单例模式:
饿汉/懒汉/DCL/枚举
工厂模式:
简单工厂/工厂方法/抽象工厂
代理模式:
JDK/CGLIB
观察者模式:
事件监听
使用场景:
根据业务选择
面试加分点(设计模式题建议这样答):
先说“解决什么问题”,再说“为什么是这个模式而不是别的模式”。
必须落到项目场景,例如:
单例:配置中心客户端、线程池管理器
工厂:多渠道支付对象创建
代理:RPC客户端桩、AOP切面
观察者:订单状态变更触发短信/积分/通知
十七、Linux 与 Git
17.1 Linux 命令
329-343题
常用命令:
ls, cd, pwd
grep, find
cat, head, tail
chmod, chown
ps, top
netstat
高频排障命令组合(可直接口述):
查CPU高占用进程:
top -Hp <pid>看Java线程栈:
jstack <pid> | grep -n "BLOCKED"查日志错误:
grep -in "error" app.log | tail -n 100查端口占用:
netstat -tunlp | grep 8080查磁盘/IO:
df -h、iostat -x 1 5
342题可执行答案:
find . -name "*.log" -print0 | xargs -0 grep -in "error" > error_log.txt
17.2 Git
344-346题
rebase vs merge:
rebase: 线性历史
merge: 保留分支历史
冲突解决:
手动编辑
git add
标准冲突处理流程:
git fetch拉最新分支git rebase origin/main(或 merge)解决冲突文件
本地测试通过后
git rebase --continue推送并发起PR
十九、分布式定时任务与接口
359-362题
幂等性:
唯一ID
状态机
限流:
计数器
令牌桶
滑动窗口
接口限流落地建议:
网关层全局限流(保护系统)
业务层细粒度限流(按用户/接口/租户)
超限返回明确错误码,便于客户端退避重试
二十、认证与安全
20.1 JWT
363-370题
JWT结构:
Header
Payload
Signature
优点:
无状态
可验证
安全问题:
过期时间
密钥安全
JWT详细防护方案:
AccessToken短期有效,RefreshToken中期有效
敏感操作强制二次校验(短信/OTP)
token中包含
jti,服务端维护黑名单实现强制下线密钥轮换与版本化(kid)
全程HTTPS,防止明文窃取
二十一、实习与职业发展
371-390题
这部分建议“真诚 + 结构化 + 与岗位匹配”。
常见问题回答模板:
为什么不留在上一家公司?
从“业务方向匹配度、成长空间、技术挑战”回答,不抱怨。
你在实习学到了什么?
讲工程化能力:需求拆解、联调、上线、复盘。
你的职业规划?
1年做深模块,3年能扛系统,5年形成领域专长。
376. 软件开发流程是什么样的?
需求评审 -> 技术方案评审 -> 开发联调 -> 测试回归 -> 灰度发布 -> 复盘
379. 如何定位系统瓶颈并优化?
先指标定位(RT/QPS/错误率)
再链路拆解(网关/应用/DB/缓存/MQ)
最后小步改造+AB验证
二十二、转码与数据处理
391-395题
392. 两种转码方法优缺点?
本地FFmpeg:灵活、成本低;但扩容和隔离能力弱
分布式转码服务:扩展性强、可观测好;但架构复杂度更高
393. 转码失败率如何监控?
指标:任务成功率、平均耗时、超时率、重试率
告警:按失败率阈值+连续时间窗口触发
394. 转码结果前端如何感知?
轮询任务状态或WebSocket推送
状态机:排队中/处理中/成功/失败
395. 同步还是异步?为什么?
大文件转码必须异步,避免请求超时和线程阻塞
同步仅用于小文件或预览场景
396. 为什么不直接存原始文件?
终端兼容性、带宽成本、播放性能、审核与水印处理等都要求统一编码格式
二十三、其他问题
396-403题
397. 常见问题与注意事项
避免“只背概念不落地”,每题最好配一个项目场景
398-399. 兼容性问题如何修复并提升成功率?
先按失败类型分桶(编码、格式、字段)
分别做兼容适配和兜底策略
通过灰度验证,观察成功率提升
400. 项目里最大困难及解决?
重点说“跨团队协作+技术方案落地+结果量化”
401. 并发导致数据不一致怎么处理?
事务隔离级别、乐观锁/悲观锁、分布式锁、消息最终一致性
402. 死锁排查常用JDK命令?
jstack <pid>(最常用)可配合
jcmd、jmap做进一步分析
403. 什么是死锁,四个条件,如何避免?
条件:互斥、占有且等待、不可剥夺、循环等待
避免:固定加锁顺序、超时锁、减少锁嵌套、无锁化改造