后端面试全栈查漏补缺

综合面试复盘:涵盖 Docker · Spring · Go · Kafka/RocketMQ · Java 并发 · 数据库 · MyBatis 七大板块。
按"易错点→正确姿势→记忆口诀"结构整理,适合面试前 24 小时速刷。


目录

  1. Docker
  2. Spring 框架
  3. Go 语言
  4. Kafka & RocketMQ
  5. Java 并发编程
  6. 数据库(MySQL)
  7. MyBatis & MyBatis-Plus

一、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 传参时被整体覆盖
对比CMDENTRYPOINT
是否可被 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 rundocker 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 CGLIBSpring Boot 2.x+ 默认全 CGLIBproxy-target-class=true

三、Go 语言

1. goroutine & GMP 模型

G = Goroutine(协程)
M = Machine(OS 内核线程)
P = Processor(逻辑处理器,数量 = GOMAXPROCS,默认 CPU 核数)
对比goroutineOS 线程
创建关系N : M(N 个 goroutine 映射到 M 个 OS 线程)1 : 1
初始栈~2KB(可动态增长)~1MB(固定)
创建/切换成本用户态,纳秒级内核态,微秒级

关键机制:

机制说明
Work StealingP 本地队列空时,从其他 P 偷 goroutine
Hand OffM 因 syscall 阻塞时,P 与 M 解绑,绑定空闲 M 继续运行
GOMAXPROCSruntime.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)
对比makenew
作用类型仅 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 同时满足随机选一个执行
零 caseselect {}永远阻塞(死锁)
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.Mutexmu.Lock() / mu.Unlock()通用,读写频率差不多
sync.RWMutexmu.RLock() / mu.RLock()读远多于写
sync.Mapm.Store() / m.Load()Go 1.9+,写一次读多次,不用手动加锁

⚠️ 易错: 你说"不知道"并发读写怎么做,以上三种记一个就行(至少 sync.Mutex 必须会)。


四、Kafka & RocketMQ

1. Kafka vs RocketMQ 核心区别

对比KafkaRocketMQ
定位高吞吐流处理平台(日志/埋点/大数据)通用消息中间件(业务消息/事务消息)
事务消息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漏了 synchronizedCAS + synchronized 锁桶头节点 + 红黑树

六、数据库(MySQL)

1. EXPLAIN 核心字段

字段重点关注说明
typeALL全表扫描(大忌)→ 需要加索引
ref / eq_ref命中非唯一/唯一索引 ✅
range索引范围查询 ✅
index遍历索引树(比 ALL 略好但也不好)
key空 = 未用索引 ❌实际使用索引名
rows数字越大越慢预估扫描行数
ExtraUsing 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

对比CHARVARCHAR
长度固定长度(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;
对比UNIONUNION 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

resultTyperesultMap
使用方式直接指定类全限定名定义 <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
Springspring.factoryspring.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掌握较好