后端面试全栈查漏补缺
综合面试复盘:涵盖 Docker · Spring · Go · Kafka/RocketMQ · Java 并发 · 数据库 · MyBatis 七大板块。
按"易错点→正确姿势→记忆口诀"结构整理,适合面试前 24 小时速刷。
目录
一、Docker
1. Docker vs 虚拟机(VM)
| 维度 | Docker 容器 | 虚拟机 |
|---|---|---|
| 内核 | 共享宿主机内核 | 每个 VM 有独立 Guest OS |
| 隔离层级 | 进程级隔离(namespace) | 硬件级虚拟化(Hypervisor) |
| 资源限制 | cgroups(不是 "groupspace"!) | 独立分配 |
| 启动时间 | 毫秒级 | 秒~分钟级 |
⚠️ 易错: "groupspace" → 正确是 cgroups(Control Groups),控制资源使用(CPU/内存/IO)。
🧠 记法: namespaces 隔离("看不着") + cgroups 限制("用不多")
2. CMD vs ENTRYPOINT
# ENTRYPOINT:容器入口命令,docker run 传参追加到其后(不可覆盖)
# CMD:提供默认参数/命令,docker run 传参时被整体覆盖
| 对比 | CMD | ENTRYPOINT |
|---|---|---|
是否可被 docker run 覆盖 | ✅ 完全覆盖 | ❌ 不可覆盖(除非 --entrypoint) |
| 常见用法 | CMD ["-c", "config.yml"] 提供默认参数 | ENTRYPOINT ["java", "-jar", "app.jar"] 固定入口 |
经典组合模式:
ENTRYPOINT ["ping"] # 固定入口
CMD ["-c", "3", "8.8.8.8"] # 默认参数
# docker run myping → ping -c 3 8.8.8.8
# docker run myping -c 10 1.1.1.1 → ping -c 10 1.1.1.1(追加)
⚠️ 易错: 之前回答 "不会"
🧠 记法: ENTRYPOINT = "固定入口"(船锚 🚢),CMD = "默认参数"(备忘录 📝)
3. docker compose vs docker run
docker run | docker compose | |
|---|---|---|
| 定义方式 | 命令行参数 | docker-compose.yml 声明式配置 |
| 适用场景 | 单个容器测试 | 多服务编排(web + db + redis + mq) |
| 网络 | 默认独立网络 | 自动创建共享网络,按服务名通信 |
❌ 易错:docker compose 不管理 Dockerfile(Dockerfile 由
docker build管理),它管的是多服务编排。
4. Docker 网络模式
| 模式 | 命令 | 说明 |
|---|---|---|
| bridge | --network bridge | 默认,容器内独立网络栈,NAT 访问外网 |
| host | --network host | 容器直接使用宿主机网络栈(无隔离) |
| none | --network none | 无网络(安全隔离场景) |
| overlay | --network overlay | ⚠️ 跨主机通信(Swarm 集群用) |
⚠️ 易错: 只说了 bridge/host/none,漏掉了 overlay 模式。
🧠 记法: 单机四选一(bridge/host/none/overlay),跨机用 overlay。
二、Spring 框架
已整理独立文件 → Spring面试查漏补缺.md
核心速记
| 知识点 | 口决 / 重点 |
|---|---|
@SpringBootApplication 组成 | 3 个注解:@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan |
| 自动配置加载路径变更 | 2.7 前 → spring.factories / 2.7+ → AutoConfiguration.imports |
| 条件注解 | 必说 3 个:@ConditionalOnClass / @OnMissingBean / @OnProperty |
| AOP | @Pointcut = 切点(在哪切),@Around = 通知类型(什么时候切) |
| JDK vs CGLIB | Spring Boot 2.x+ 默认全 CGLIB(proxy-target-class=true) |
三、Go 语言
1. goroutine & GMP 模型
G = Goroutine(协程)
M = Machine(OS 内核线程)
P = Processor(逻辑处理器,数量 = GOMAXPROCS,默认 CPU 核数)
| 对比 | goroutine | OS 线程 |
|---|---|---|
| 创建关系 | N : M(N 个 goroutine 映射到 M 个 OS 线程) | 1 : 1 |
| 初始栈 | ~2KB(可动态增长) | ~1MB(固定) |
| 创建/切换成本 | 用户态,纳秒级 | 内核态,微秒级 |
关键机制:
| 机制 | 说明 |
|---|---|
| Work Stealing | P 本地队列空时,从其他 P 偷 goroutine |
| Hand Off | M 因 syscall 阻塞时,P 与 M 解绑,绑定空闲 M 继续运行 |
| GOMAXPROCS | runtime.GOMAXPROCS(N) = 系统中 P 的最大数量 |
⚠️ 遗漏: 没说到初始栈 ~2KB、
GOMAXPROCS、Hand Off 机制。
2. defer(高频面试)
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
panic("panic!")
defer fmt.Println("4") // ❌ 这行不会被执行(panic 之后的 defer 不注册)
输出:
3
2
1
panic: panic!
⚠️ 易错(你答了 4321 ❌): panic 之后代码不执行,defer 按 LIFO(后进先出)执行已注册的。所以是 3 2 1,不是 4321。
recover 能否捕获 panic?
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获了 panic:", r) // ✅ 可以捕获
}
}()
panic("出错了")
// 输出:捕获了 panic: 出错了
⚠️ 易错(你说的"不能捕获" ❌):
recover()在 defer 函数中直接调用就能捕获 panic,防止程序崩溃!
| 你的回答 | 纠正 |
|---|---|
| 顺序 4321 ❌ | 3 2 1(panic 后的 defer 4 不注册) |
| defer 里 recover 不能捕获 ❌ | ✅ 可以捕获(条件是 recover 在 defer 中直接调用) |
3. make vs new
make([]int, 5) // ✅ 返回已初始化的 slice(len=5, cap=5)
make(map[int]int) // ✅ 返回已初始化的 map,可直接赋值
make(chan int) // ✅ 返回已初始化的 channel
new(int) // 返回 *int,值为 0
new([]int) // 返回 *[]int,值为 nil slice(⚠️ 不能直接 append)
new(map[int]int) // 返回 *map[int]int,值为 nil map(⚠️ 直接赋值 panic)
| 对比 | make | new |
|---|---|---|
| 作用类型 | 仅 slice / map / channel | 任意类型 |
| 返回值 | T(初始化后的值) | *T(指针) |
| 底层 | 分配 + 初始化(非零值) | 仅分配零值内存 |
🧠 记法:
make是"开张"(店开门,货架摆好),new是"圈地"(只买了块地,什么都没盖)
4. interface{} / any
func Foo(v interface{}) // ✅ 标准写法:传值即可
func Bar(v *interface{}) // ❌ 反模式!几乎从不需要
⚠️ *易错(你说的选 interface{} ❌): 这是 Go 中经典误解。
*interface{}在 99% 场景是代码坏味道。
正确做法: 用 interface{} 接收任意类型,用具体类型指针实现可变:
func Foo(v interface{}) // ✅ 接收任意类型
func Set(v *int) // ✅ 要修改外部值,用具体类型指针
5. channel 死锁(经典题)
ch := make(chan int) // 无缓冲 channel
ch <- 1 // ⛔ 阻塞!同 goroutine 内发送,没有接收方
fmt.Println(<-ch) // 永远到不了这行
// 结果: fatal error: all goroutines are asleep - deadlock!
如何修复?
// 方案 A:用 goroutine 隔开发送和接收
go func() { ch <- 1 }()
fmt.Println(<-ch) // ✅ main goroutine 接收
// 方案 B:有缓冲 channel
ch := make(chan int, 1)
ch <- 1 // ✅ 缓冲还有空间,不阻塞
fmt.Println(<-ch)
⚠️ 易错: 当时说那代码不会死锁,实际上无缓冲 channel 在同一 goroutine 中先发后收直接死锁。
6. select(随机公平选择)
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
case <-ch3:
fmt.Println("ch3")
⚠️ 易错(你说的"优先执行第一个" ❌): 当多个 case 同时满足条件时,Go 的
select随机公平选择,不是按顺序!这是为了防止前面的 case 饿死后面的。
| 特性 | 说明 |
|---|---|
| 多个 case 同时满足 | 随机选一个执行 |
| 零 case | select {} — 永远阻塞(死锁) |
| default | 所有 case 都不满足时走 default(非阻塞) |
| 超时模式 | case <-time.After(3 * time.Second) |
7. map 并发安全
var m = make(map[string]int)
go func() { m["key"] = 1 }() // ❌ 并发写 → fatal error
go func() { m["key"] = 2 }() // ❌
原生 map 不是并发安全的! 并发读写会 fatal error(不是 panic,无法 recover)。
三种解决方案:
| 方案 | 代码 | 适合场景 |
|---|---|---|
sync.Mutex | mu.Lock() / mu.Unlock() | 通用,读写频率差不多 |
sync.RWMutex | mu.RLock() / mu.RLock() | 读远多于写 |
sync.Map | m.Store() / m.Load() | Go 1.9+,写一次读多次,不用手动加锁 |
⚠️ 易错: 你说"不知道"并发读写怎么做,以上三种记一个就行(至少
sync.Mutex必须会)。
四、Kafka & RocketMQ
1. Kafka vs RocketMQ 核心区别
| 对比 | Kafka | RocketMQ |
|---|---|---|
| 定位 | 高吞吐流处理平台(日志/埋点/大数据) | 通用消息中间件(业务消息/事务消息) |
| 事务消息 | ✅ Kafka 0.11+ 支持(Transactional API) | ✅ 原生支持 |
| 顺序消息 | 分区内有序(生产端需指定 key) | 支持全局/分区有序 |
| 延迟消息 | ❌ 不支持(社区版) | ✅ 支持 18 个延迟级别 |
| 消费模式 | pull 模式 | pull + push 模式 |
⚠️ 易错(你说的"Kafka 不支持事务" ❌): Kafka 0.11(KIP-98)起就支持原生事务,通过
producer.initTransactions()+producer.beginTransaction()+producer.commitTransaction()实现。
2. Kafka 可靠性保证(3 件套)
生产者:acks=all(要等待所有副本确认)
broker:min.insync.replicas=2(最少同步副本数)
消费者:手动提交 offset(enable.auto.commit=false)
+ idempotence:enable.idempotence=true(幂等,防重复)
⚠️ 易错: 之前回答太简略,没说到"3 个环节(生产端/Broker/消费端)各自如何保证可靠性"。
3. Kafka 高性能原因
| 机制 | 说明 |
|---|---|
| 顺序写磁盘 | Kafka 消息追加到 partition 文件末尾,顺序 I/O ≈ 内存写入速度 |
| 零拷贝(sendfile) | 数据从 PageCache → socket(不经过用户态),减少 2 次上下文切换 + 1 次内存拷贝 |
| Page Cache | 利用 OS 的磁盘缓存,读命中率极高 |
| 批量压缩 | 生产者批量发送、Broker 批量存储、消费者批量拉取(减少网络 IO) |
| 分区并行 | 一个 topic 多个 partition,消费端可多线程并行消费 |
⚠️ 易错(说的"没有消费者的 ack 确认" ❌): Kafka 消费者通过 commit offset 来确认消费位置,等价于 ack 机制。手动 commit 配合
enable.auto.commit=false可精确控制"消费到哪了"。
五、Java 并发编程
已整理独立文件 → Java并发面试查漏补缺.md
核心速记
| 知识点 | 你的错 | 纠正 |
|---|---|---|
| Stream API | "语法糖" ❌ | Pipeline 机制 + 惰性求值 + 短路优化,运行时真实优化 |
| ReentrantLock | "重量级锁" ❌ | AQS + CAS 实现的轻量级锁(synchronized 早期是重量级) |
| volatile 可见性方向 | 说反了 ❌ | 写→刷回主存,读→从主存重载 |
| volatile 原子性 | 不知道 ❌ | 不能保证原子性(i++ 三步操作) |
| ConcurrentHashMap JDK8 | 漏了 synchronized | CAS + synchronized 锁桶头节点 + 红黑树 |
六、数据库(MySQL)
1. EXPLAIN 核心字段
| 字段 | 重点关注 | 说明 |
|---|---|---|
| type | ALL | 全表扫描(大忌)→ 需要加索引 |
| ref / eq_ref | 命中非唯一/唯一索引 ✅ | |
| range | 索引范围查询 ✅ | |
| index | 遍历索引树(比 ALL 略好但也不好) | |
| key | 空 = 未用索引 ❌ | 实际使用索引名 |
| rows | 数字越大越慢 | 预估扫描行数 |
| Extra | Using filesort | 文件排序(需优化 Order By 索引) |
| Using temporary | 临时表(需优化 Group By 索引) | |
| Using index | 覆盖索引 ✅(不回表) |
⚠️ 易错: 只说"看 type"、"看 rows"太笼统,至少要说出 type 的几种级别排序(system > const > eq_ref > ref > range > index > ALL),以及 Extra 中的 Using filesort / Using temporary 是慢查询信号。
2. JOIN 类型
-- 🔹 INNER JOIN:两表都匹配的记录(交集)
SELECT * FROM A INNER JOIN B ON A.id = B.a_id;
-- 🔹 LEFT JOIN:左表全部 + 右表匹配的记录(无匹配则 NULL)
SELECT * FROM A LEFT JOIN B ON A.id = B.a_id;
-- 🔹 RIGHT JOIN:右表全部 + 左表匹配的记录(无匹配则 NULL)
SELECT * FROM A RIGHT JOIN B ON A.id = B.a_id;
-- 🔹 CROSS JOIN:笛卡尔积(两表所有组合),慎用!
SELECT * FROM A CROSS JOIN B;
-- ⚠️ MySQL 不支持 FULL OUTER JOIN,需用 UNION 实现
⚠️ 易错: 漏了 CROSS JOIN(笛卡尔积),把"外联"算成一种 → 实际上外联分 LEFT / RIGHT / FULL 三种。FULL OUTER JOIN 要记住 MySQL 不支持,用
LEFT JOIN+UNION+RIGHT JOIN模拟。
3. CHAR vs VARCHAR
| 对比 | CHAR | VARCHAR |
|---|---|---|
| 长度 | 固定长度(CHAR(10) 一律占 10 字符) | 可变长度(存多少占多少 + 1~2 字节长度前缀) |
| 最大长度 | 255 字符 | 65535 字节(约 21845 个 UTF-8 汉字) |
| 尾部空格 | 自动截断(读出来不带空格) | 保留(读出来原样带空格) |
| 适用场景 | 固定长度字段(手机号/身份证/性别) | 变长字段(用户名/标题/地址) |
⚠️ 易错: 没说尾部空格处理差异(CHAR 自动截断 vs VARCHAR 保留)。这个细节面试官经常追问。
4. UNION vs UNION ALL
SELECT name FROM employees
UNION -- ❌ 自动去重(会多一步排序去重,性能差)
SELECT name FROM ex_employees;
SELECT name FROM employees
UNION ALL -- ✅ 直接拼接,不去重,性能更好
SELECT name FROM ex_employees;
| 对比 | UNION | UNION ALL |
|---|---|---|
| 去重 | ✅ 自动去重(有排序开销) | ❌ 不处理重复 |
| 性能 | 慢(额外排序 + 去重) | 快(直接拼接结果) |
| 适用 | 确实需要去重 | 大多数场景(不需要去重时优先用) |
⚠️ 易错(回答"不清楚"): 记住 UNION 去重但慢,UNION ALL 快。业务中数量级不大再用 UNION,大数据量默认 UNION ALL + 自己控制去重。
七、MyBatis & MyBatis-Plus
1. #{} vs ${}
<!-- #{} → 预编译(PreparedStatement),SQL 注入安全 ✅ -->
<select id="getUser">
SELECT * FROM user WHERE id = #{id}
<!-- 实际执行: SELECT * FROM user WHERE id = ? → 参数安全设置 -->
</select>
<!-- ${} → 字符串拼接,有 SQL 注入风险 ❌ -->
<select id="searchUser">
SELECT * FROM ${tableName} <!-- 仅用于表名/列名等动态 SQL -->
</select>
| 对比 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译占位符 | 字符串直接替换 |
| SQL 注入 | ✅ 安全 | ❌ 有风险 |
| 使用场景 | 参数值 | 表名/列名/ORDER BY 等动态片段 |
| 执行过程 | SELECT * FROM user WHERE id = ? → 参数设置 | SELECT * FROM user WHERE id = 1 → 直接拼入 SQL |
⚠️ 易错(你说的"{} 是占位符" ❌):** {} 是字符串直接拼接**,不是占位符。只有
#{}才是 JDBC 的?占位符。
2. 缓存机制
| 级别 | 作用域 | 默认 | 说明 |
|---|---|---|---|
| 一级缓存(L1) | SqlSession | ✅ 开启 | 同一个 SqlSession 内,相同 SQL 查一次后缓存 |
| 二级缓存(L2) | namespace / Mapper | ❌ 关闭 | 需手动开启 <cache/>,跨 SqlSession 共享 |
⚠️ 注意: 二级缓存有脏读问题(不同 Mapper 操作同表时,缓存不同步),分布式环境无效(缓存存在单机内存里)。面试时说出这两点非常加分。
✅ 这题你答得最好——整个面试中得分最高的一题。
3. resultType vs resultMap
resultType | resultMap | |
|---|---|---|
| 使用方式 | 直接指定类全限定名 | 定义 <resultMap> 节点 |
| 字段映射 | 自动映射(同名匹配,开启驼峰则 user_name → userName) | 手动映射(可配置字段名→属性名的映射关系) |
| 复杂映射 | ❌ 不支持关联查询 | ✅ <association>(一对一)、<collection>(一对多) |
| 典型场景 | 字段名和属性名一致、简单查询 | 字段名不一致 / 多表关联 / 嵌套查询 |
<!-- ✅ 简单查询用 resultType -->
<select id="getUser" resultType="com.example.User">
SELECT id, name, email FROM user WHERE id = #{id}
</select>
<!-- ✅ 复杂映射用 resultMap -->
<resultMap id="UserMap" type="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<association property="dept" javaType="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
</association>
</resultMap>
⚠️ 易错(你说的"不会"): 见上表。简单记:字段名一致 → resultType,不一致 / 复杂关联 → resultMap。
附:面试失分点全景图
💀 最该补的(答得最差的)
| 排名 | 知识点 | 问题 |
|---|---|---|
| 🥇 | Go defer(492 错误) + recover | 四个全错 |
| 🥇 | Go *interface{} | 选反了 |
| 🥇 | MySQL UNION vs UNION ALL | 说"不清楚" |
| 🥇 | resultType vs resultMap | 说"不会" |
| 🥇 | Docker CMD vs ENTRYPOINT | 说"不会" |
| 🥇 | Kafka 事务 | 说 Kafka 不支持(实际 0.11+ 支持) |
📌 术语严重不准确的
| 场景 | 你说的 | 正确 |
|---|---|---|
| Docker | "groupspace" | cgroups |
| Spring | @AutoConfiguration | @EnableAutoConfiguration |
| Spring | spring.factory | spring.factories(缺 s) |
| Spring | @OnClassBean | @ConditionalOnClass... 要具体说 |
| Go web 框架 | 无(但接口特性答不出) | Go 隐式实现(duck typing) |
| Java 并发 | 说 ReentrantLock 重量级锁 | AQS + CAS 轻量级锁 |
| Java 并发 | volatile 方向说反 | 写→主存,读→从主存 |
| Java 并发 | "本地内存" | JMM 术语:主存 + 工作内存 |
✅ 答得好的(继续保持)
| 知识点 | 评价 |
|---|---|
| Docker 网络模式(bridge/host/none) | 基本全对,漏 overlay |
| Docker vs VM 对比 | 概念清晰,改口 cgroups 后没问题 |
| MyBatis 一/二级缓存 | 全场最佳,细节到位 |
| MyBatis-Plus 核心优势 | 答得全面,有实践经验 |
| GMP 模型 work stealing | 思路对,补充初识栈大小更好 |
| make vs new | 基本全对 |
| synchronized 锁升级 | 流程清楚 |
| ConcurrentHashMap | 掌握较好 |
后端面试总结
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法