跳到主要内容

后端开发面经

核心考点

1. 数据库

索引

  • B+ 树索引:叶节点存数据,范围查询高效
  • 联合索引遵循「最左前缀」原则
  • 覆盖索引:查询字段全在索引中,避免回表
  • 避免索引失效:函数操作、隐式类型转换、% 开头的 LIKE

事务 ACID

  • Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)
  • 隔离级别:读未提交 → 读已提交 → 可重复读(MySQL 默认)→ 串行化
  • MVCC:通过 undo log + Read View 实现无锁并发读

2. Redis

  • 数据类型:String、Hash、List、Set、ZSet(跳表)、Stream
  • 持久化:RDB(快照,恢复快)vs AOF(追加日志,数据更完整)
  • 缓存问题
    • 穿透:查不存在的 key → 布隆过滤器 / 缓存空值
    • 击穿:热点 key 过期 → 互斥锁 / 永不过期
    • 雪崩:大量 key 同时过期 → 随机 TTL / 熔断
  • 分布式锁:SET key value NX EX 或 Redlock

3. 分布式系统

  • CAP 定理:一致性、可用性、分区容忍性,三选二
  • BASE 理论:基本可用、软状态、最终一致性
  • 分布式事务:2PC、TCC、Saga、消息最终一致性
  • 限流算法:令牌桶、漏桶、固定窗口、滑动窗口

4. 消息队列

对比KafkaRabbitMQRocketMQ
吞吐量极高中等
顺序消息分区内有序不保证支持
延迟消息不支持插件支持原生支持
适合场景日志、大数据异步解耦电商、金融

5. 微服务

  • 服务发现:Consul、Nacos、Eureka
  • API 网关:认证、限流、路由、聚合(Kong、APISIX)
  • 熔断降级:Hystrix / Sentinel,防止级联故障
  • 链路追踪:Jaeger、Zipkin、SkyWalking

6. 高频面试题

  • TCP 三次握手 / 四次挥手:建立/关闭连接,TIME_WAIT 的作用
  • HTTP vs HTTPS:HTTPS = HTTP + TLS,证书验证、对称/非对称加密
  • JWT 原理:Header.Payload.Signature,无状态认证,注意续期和吊销
  • 数据库分库分表:垂直拆分(按业务)vs 水平拆分(按数据量),ShardingSphere
  • 接口幂等性:唯一请求ID、数据库唯一约束、状态机

MySQL 深入面经

MySQL 核心知识

1. 存储引擎

特性InnoDBMyISAM
事务
外键
行级锁❌(表锁)
崩溃恢复
全文索引✅(5.6+)
适用场景OLTP读多写少

2. 索引深入

B+ 树结构

  • 非叶子节点只存 key,叶子节点存 key + data
  • 叶子节点通过链表相连,范围查询只需遍历链表
  • 树高度通常 2-4 层,亿级数据 IO 次数少

聚簇索引 vs 二级索引

  • 聚簇索引(主键):叶子节点直接存完整行数据
  • 二级索引:叶子节点存主键值 → 需回表查完整数据
  • 覆盖索引:查询字段全在索引中,无需回表

索引失效场景

-- ❌ 函数操作
WHERE YEAR(create_time) = 2024
-- ✅ 改为范围查询
WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'

-- ❌ 隐式类型转换(字段为 varchar,值为数字)
WHERE phone = 13800138000
-- ✅
WHERE phone = '13800138000'

-- ❌ 联合索引 (a, b, c),跳过了 b
WHERE a = 1 AND c = 3

3. 事务与锁

MVCC 实现细节

  • 每行数据有隐藏字段:trx_id(最后修改的事务ID)、roll_pointer(指向 undo log)
  • Read View:包含活跃事务列表,决定当前事务能看哪个版本
  • 可重复读:事务开始时创建 Read View,整个事务期间复用
  • 读已提交:每次 SELECT 创建新 Read View

锁类型

共享锁(S):SELECT ... LOCK IN SHARE MODE
排他锁(X):SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE
意向锁(IS/IX):表级,InnoDB 自动加,表示行锁存在
间隙锁(Gap Lock):锁定索引间隙,防止幻读(可重复读级别)
临键锁(Next-Key Lock)= 行锁 + 间隙锁

死锁

  • 两个事务互相等待对方持有的锁
  • InnoDB 自动检测,回滚代价小的事务
  • 预防:固定加锁顺序、减少锁范围、缩短事务

4. EXPLAIN 分析

EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
字段关注点
typesystem > const > ref > range > index > ALL(ALL 是全表扫描,需优化)
key实际使用的索引
rows预计扫描行数(越小越好)
ExtraUsing filesort(文件排序,需优化)/ Using temporary / Using index(覆盖索引,好)

5. 主从复制与高可用

复制原理

Master binlog → Slave IO Thread → relay log → Slave SQL Thread → 执行
  • binlog 格式:STATEMENT(可能不一致)/ ROW(准确)/ MIXED
  • 半同步复制:至少一个从库收到 binlog 再提交,保证数据不丢
  • MGR(MySQL Group Replication):多主复制,Paxos 协议保证一致性

6. 高频面试题

  • 为什么推荐自增主键? 顺序写入 B+ 树,避免页分裂,减少碎片
  • 大表如何 DDL 变更? pt-online-schema-change / gh-ost,在线变更不锁表
  • 慢查询如何定位? 开启 slow_query_log + EXPLAIN 分析 + 优化索引
  • count(*) 和 count(1) 有区别吗? InnoDB 中几乎没有区别,都走最小索引全扫描
  • 为什么 NOT IN 会导致索引失效? 含 NULL 值时 NOT IN 结果不确定,可改用 NOT EXISTS

分布式系统设计面经

分布式核心知识

1. 分布式 ID 生成

方案优点缺点
数据库自增简单有序单点,性能差
UUID无需协调无序,存储空间大
雪花算法(Snowflake)高性能、趋势递增依赖时钟,时钟回拨有风险
号段模式(Leaf)DB 友好需 DB,有号段浪费
Redis INCR简单Redis 持久化有风险

雪花算法结构(64 bit)

1 bit (符号位=0) | 41 bit (时间戳ms) | 10 bit (机器ID) | 12 bit (序列号)
  • 可用约 69 年,单机每毫秒 4096 个 ID

2. 分布式锁

Redis 实现

-- 加锁
SET lock_key unique_value NX EX 30

-- 释放锁(Lua 脚本保证原子性)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
  • 锁续期(看门狗):Redisson 默认 30s 过期,持锁期间每 10s 续期
  • Redlock:向 N 个独立 Redis 实例加锁,超过半数成功则加锁成功

ZooKeeper 实现

  • 创建临时有序节点,序号最小的获得锁
  • 监听前一个节点删除事件,避免羊群效应

3. 分布式事务

2PC(两阶段提交)

阶段1:协调者发 Prepare → 各参与者执行事务(不提交)并返回 Yes/No
阶段2:全部 YesCommit;任一 NoRollback
  • 问题:协调者单点,阻塞协议,网络分区时数据不一致

TCC(Try-Confirm-Cancel)

  • Try:预占资源(冻结库存)
  • Confirm:确认提交(扣减库存)
  • Cancel:取消回滚(解冻库存)
  • 业务侵入性强,需实现三个接口

Saga

  • 长事务拆成多个本地事务,每步有对应补偿事务
  • 编排模式(Choreography)vs 指挥模式(Orchestration)

消息最终一致性

本地事务 + 发消息 → MQ → 消费者执行 → 失败则重试
  • 使用事务消息(RocketMQ)保证发消息和本地事务原子性

4. 一致性哈希

  • 将节点和数据 key 都映射到 0~2³² 的环上
  • key 顺时针找到第一个节点
  • 增减节点只影响相邻节点的数据迁移
  • 虚拟节点:每个物理节点映射多个虚拟节点,负载更均衡

5. 分布式缓存架构

多级缓存

请求 → 本地缓存(Caffeine)→ 分布式缓存(Redis)→ 数据库

缓存与 DB 一致性策略

  • Cache Aside:读:先缓存后 DB;写:先更新 DB,再删缓存
  • Write Through:写 DB 和缓存同步进行(强一致但慢)
  • 延迟双删:更新 DB → 删缓存 → 延迟 500ms 再删一次

6. 服务治理

熔断器状态机

Closed(正常)→ [失败率超阈值]Open(熔断)
[等待超时]
Half-Open(试探)
[成功]
Closed

限流算法对比

算法特点
固定窗口简单,临界点突刺问题
滑动窗口解决突刺,内存稍多
漏桶平滑输出,不允许突发
令牌桶允许一定突发,更常用

7. 高频面试题

  • BASE 和 ACID 的关系? BASE 是 CAP 中 AP 的实践,用最终一致性换可用性
  • 如何保证消息不丢失? 生产者确认 + MQ 持久化 + 消费者手动 ack
  • 如何保证消息幂等? 消费端去重(唯一 ID + 数据库唯一约束 / Redis SET NX)
  • Raft 和 Paxos 的区别? Raft 更易理解,强 Leader,leader 处理所有请求;Paxos 更通用复杂

Redis 深入面经

Redis 深水区知识

1. 数据结构底层实现

类型底层结构编码
StringSDS(简单动态字符串)int/embstr/raw
Listquicklist(小节点用 ziplist,大节点用 listpack)-
Hashziplist(小)/ hashtable(大)-
Setintset(小整数)/ hashtable-
ZSetziplist(小)/ skiplist+hashtable-

SDS vs C 字符串

  • 获取长度 O(1)(存储 len)vs O(n)
  • 二进制安全,可存储任意数据
  • 空间预分配 + 惰性释放,减少内存重分配

跳表(skiplist)

  • 多层有序链表,平均查询 O(log n)
  • 第一层包含所有节点,每层节点数约为下层的 1/2
  • 为什么不用红黑树:实现简单、支持范围查询更自然

2. 持久化深入

RDB 快照

Save 条件:
- save 900 1900秒内至少 1 个键变化
- save 300 10300秒内至少 10 个键变化

枯写过程:
fork() -> 子进程生成 RDB -> 枯写完成 -> 替换旧文件

COW(写时复制):
- 子进程共享应用程序内存页
- 主进程写入时才复制页,避免锁内存展层

AOF 日志

Append-Only File:记录每个写操作命令

fsync 策略:
- always:每次写操作后刷盘(最安全,最慢)
- everysec:每秒刷盘(默认,最多丢失 1s 数据)
- no:由操作系统决定(最快,首机可能丢失较多)

AOF 重写:
- 多个操作合并为最终状态(如 INCR 100-> SET key 100
- bgrewriteaof 命令或自动触发

RDB + AOF 混合持久化(Redis 4.0+)

  • AOF 文件前半序是 RDB 快照,后半是增量 AOF 日志
  • 兼具两者优点:加载快 + 数据完整

3. 集群模式

主从复制

Master通过 replication backlog 同步数据到 Slave

全量同步:Slave 首次连接,Master bgsave + 发送 RDB
部分同步:断线重连后,通过 offset 发送市机进山的命令

哨兵(Sentinel)

  • 监控主从状态,Master 下线后自动故障转移
  • 需超过半数 Sentinel 同意才进行故障转移(防脑裂)
  • 客户端连接 Sentinel,由 Sentinel 告知当前 Master 地址

Redis Cluster

16384 个哈希槽(slot)分配到各节点
计算:crc16(key) % 16384

常见分配:3 Master + 3 Slave
Master1: slot 0-5461
Master2: slot 5462-10922
Master3: slot 10923-16383

客户端请求错误节点时接收 MOVED 指引到正确节点

4. 缓存三大问题深度分析

缓存击空庄(Penetration)

问题:不存在的 key 每次请求都到达 DB
解决:
1. 布隆过滤器(布隆过滤器如不存在则拦截)
2. 缓存空对象(设置较短的 TTL
3. 请求参数校验居前,非法参数直接拒绝

缓存击空空(Breakdown)

问题:热点 key 失效,大量并发请求唠向 DB
解决:
1. 互斥锁:缓存失效时只有一个线程去查 DB并回写缓存
2. 预热:后台定时探测前主动刷新缓存
3. 逻辑过期:缓存反回旧数据,后台异步更新

缓存雪崩(Avalanche)

问题:大量 key 同时失效,或 Redis 整体崩溃
解决:
1. TTL 加随机偶尔少,防止同时失效
2. 数据预加载:应用启动时预先加载热点数据
3. 熔断降级: Redis 携带时返回默认值而非打援数据库
4. 集群化插件:防止单点故障

5. 常用模式与实现

分布式锁

-- 加锁:SET key value NX EX timeout
local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
if result then return 1 else return 0 end

-- 释放锁:比较 value 后删除
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0

限流实现

-- 滑动窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 删除窗口外的请求
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计窗口内请求数
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1 -- 允许
end
return 0 -- 限流

排行榜(ZSet)

ZADD leaderboard <score> <userId>
ZRANGEBYSCORE leaderboard -inf +inf WITHSCORES -- 按分数查询
ZRANK leaderboard <userId> -- 获取排名

6. 资深面试题

  • Redis 为什么首选跳表而不用平衡树?
    • 平均 O(log n) 查询和平衡树相同,但实现简单、内存占用小
    • 跳表有天然的范围查询优势,ZRANGEBYSCORE 非常高效
    • 无需旋转回平衡,写入性能更好
  • Redis 单线程执行为什么还这么快?
    • 除 IO 外都是内存操作,没有磁盘寻址
    • 非阻塞 IO 多路复用,一个线程处理所有请求
    • 单线程避免了锁竭争和上下文切换开销
  • RESP 协议是什么?
    • Redis 客户端服务器通信协议。类型标志:+成功 -错误 $字符串 *数组 :整数
    • 为什么选文本協议:易于调试、应用程序层天然支持、解析简单
  • Pipeline 和 Lua 脚本的区别?
    • Pipeline:客户端批量发送命令,减少网络往返;我不保证原子性
    • Lua 脚本:在 Redis 服务端执行,多个命令完全原子
    • 需要原子性用 Lua,只是减少网络往返用 Pipeline

Go 语言核心面经

Go 语言面试重点

1. Goroutine 与调度器

GMP 模型

G (Goroutine):Go 协程,轻量线程,初始堆 2KB
M (Machine):操作系统线程
P (Processor):调度器,可运行的 goroutine 队列

每个 P 有本地队列 + 全局队列
M 必须持有 P 才能执行 G
G 发生系统调用阻塞时,MP 分离,其他 M 接管 P
工作窃取:P 的本地队列空时从其他 P 的队列尾部窃取

Goroutine 泄漏

// 常见泄漏场景
// 1. channel 没有接收者
ch := make(chan int)
go func() { ch <- 1 }() // 永远阻塞

// 2. goroutine 等待永远不会满足的条件
go func() {
select {
case <- neverClosedChannel:
}
}()

// 解决:始终将 context 传入
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok { return }
process(v)
case <-ctx.Done(): // 外部取消
return
}
}
}

2. Channel 深入

// channel 类型
// 无缓冲:发送和接收必须同时准备
ch1 := make(chan int)
// 有缓冲:发送在缓冲满之前不阻塞
ch2 := make(chan int, 10)

// select 实现多路合并
func fanIn(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range cs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}

// 超时控制
select {
case result := <-ch:
process(result)
case <-time.After(3 * time.Second):
fmt.Println("超时")
}

3. 内存管理与 GC

逃逸分析

// 编译器分析变量是分配在栈还是堆上
// 堆:生命周期超过定义作用域的变量
// 栈:局部变量,不逆逸

func bad() *int { // bad: n 逆逸到堆
func bad() *int {
n := 42 // n 逆逸到堆
return &n
}

func good() int { // good: 返回値
n := 42
return n // n 在栈上
}

// 查看逆逸
go build -gcflags='-m' ./...

GC 三色标记清除

白色:待扫描,可能被回收
N 色:正在扫描
N 色:已扫描,确认存活

STW1 -> 标记根节点 ->
并发标记(与业务 goroutine 并行)->
STW2(保添操作)-> 清除白色

写屏障:并发标记期间,对黑色对象的指针修改会标记

4. 接口与多态

// Go 接口是隐式实现的
type Reader interface {
Read(p []byte) (n int, err error)
}

// 任何有 Read 方法的类型都实现了 Reader
type MyReader struct{}
func (r MyReader) Read(p []byte) (n int, err error) { ... }

// 空接口和类型断言
var i interface{} = "hello"
s, ok := i.(string) // 类型断言

switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
}

// 接口的内部表示:(type, value) 元组
// 注意 nil 接口 != 具有 nil 值的接口
var p *int = nil
var i interface{} = p
fmt.Println(i == nil) // false!(type 不为 nil)

5. 并发模式

// Worker Pool
func workerPool(jobs <-chan Job, results chan<- Result, n int) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() { wg.Wait(); close(results) }()
}

// sync.Once 实现单例
type Singleton struct{}
var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
once.Do(func() { instance = &Singleton{} })
return instance
}

// sync.Pool 对象池
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
// 使用 buf
}

6. 资深面试题

  • Go 的 GC 会 STW 多久?
    • Go 1.14+ STW 最大几百微秒,大部分工作并发执行
    • 调控 GOGC 环境变量控制 GC 触发阈值(默认 100%)
  • make 和 new 的区别?
    • new(T) 分配 T 类型的内存并返回指针,内存为零值
    • make(T, ...) 仅用于 slice/map/channel,返回 T 类型(非指针)
  • 为什么 slice 并发不安全?
    • slice 扩容时会重新分配底层数组,并发修改可能操作舍弃的旧内存
    • 采用 sync.Mutex 或 channel 保护
  • defer 的执行顺序和性能问题?
    • LIFO 顺序执行
    • defer 在函数返回时执行,可修改命名返回值
    • 每个 defer 有一小撑开销,循环中大量使用时用匿名函数包裹

Node.js 核心面经

Node.js 深水区知识

1. 事件循环深度

6 个阶段

timers -> setTimeout / setInterval
pending I/O -> 上一转延迟的 I/O
idle, prepare -> 内部使用
poll -> 检索新 I/O,执行 I/O 回调
check -> setImmediate
close callbacks -> socket.destroy()

每个阶段之前,清空 microtask 队列:
process.nextTick 队列 -> Promise 微任务队列

经典面试题:输出顺序

setTimeout(() => console.log('setTimeout'), 0)
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
console.log('sync')

// 输出顺序:
// sync
// nextTick
// Promise
// setTimeout (在 I/O 外面,timeout 和 immediate 顺序不定)
// setImmediate

poll 阶段详解

  • 如果 timer 队列非空,进入 timers 阶段
  • 如果进入 poll 时没有 timer,直接执行 poll 回调
  • 如果 poll 为空且有 setImmediate,进入 check 阶段

2. 模块系统

CommonJS vs ESM

// CommonJS:运行时加载,同步
const fs = require('fs') // 运行时才执行
exports.greet = name => `Hello ${name}`

// ESM:静态分析,异步
import fs from 'fs' // 编译时分析
export const greet = name => `Hello ${name}`

// 主要区别
// 1. CommonJS 是值导出,ESM 是实时绑定
// 2. CommonJS require 可在中途调用,import 必须在顶层
// 3. ESM 有利于 tree-shaking
const value = require('./module').value // CommonJS: value 是拷贝
import { value } from './module' // ESM: value 是引用

模块寻址顺序

1. 核心模块(Buffer、path...
2. 路径模块(./..//开头)
3. node_modules 查找(逐级向上)

3. 性能优化

集群模式

// cluster 模块利用多核 CPU
import cluster from 'cluster'
import { cpus } from 'os'

if (cluster.isPrimary) {
const numCPUs = cpus().length
for (let i = 0; i < numCPUs; i++) {
cluster.fork() // 每个 CPU 核开一个 Worker
}
cluster.on('exit', (worker) => {
cluster.fork() // Worker 崩溃自动重启
})
} else {
// Worker 进程运行 HTTP 服务
app.listen(3000)
}

Worker Threads 处理 CPU 密集任务

import { Worker, isMainThread, parentPort } from 'worker_threads'

if (isMainThread) {
const worker = new Worker(__filename)
worker.on('message', result => console.log('Result:', result))
worker.postMessage({ data: heavyData })
} else {
parentPort.on('message', ({ data }) => {
const result = heavyComputation(data) // CPU 密集运算
parentPort.postMessage(result)
})
}

内存泄漏排查

# 开启内存分析
node --inspect app.js

# 姳片分析
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>

# 常见泄漏场景:
# 1. 全局变量累积
# 2. 闭包持有外部引用未释放
# 3. 事件监听器未移除

4. 流处理与管餅

// 流序列化:管餅
readStream
.pipe(gunzip()) // 解压
.pipe(csvParser()) // 解析
.pipe(transform()) // 转换
.pipe(writeStream) // 写入

// 现代写法:pipeline
const { pipeline } = require('stream/promises')
await pipeline(
fs.createReadStream('input.gz'),
zlib.createGunzip(),
fs.createWriteStream('output')
)
// 自动处理错误和清理

// 备压处理
readStream.on('data', chunk => {
if (!writeStream.write(chunk)) {
readStream.pause() // 写入缓冲达到高水位,暂停读取
writeStream.once('drain', () => readStream.resume())
}
})

5. 资深面试题

  • 如何处理运算密集任务而不阻塞事件循环?
    • Worker Threads 处理 CPU 密集
    • child_process.fork() 开子进程
    • 将任务切片,用 setImmediate/setTimeout 分批执行
  • require 模块是否每次都重新执行?
    • 不是,模块有缓存机制(require.cache)
    • 第一次 require 执行并缓存,后续直接返回缓存
    • 删除缓存:delete require.cache[require.resolve('./module')]
  • 如何优化 Node.js 应用的内存占用?
    • 调整 --max-old-space-size 设置堆内存上限
    • 大文件用 Stream 处理,避免全部读入内存
    • 缓存设置合理的 TTL,防止无限增长
  • process.nextTick 和 setImmediate 的应用场景?
    • nextTick:当前操作完成后立即执行,高于其他微任务
    • setImmediate:在当前 poll 阶段完成后执行
    • nextTick 如果递归调用可能阻塞事件循环,要注意

Java / Spring Boot 核心面经

Java 运行时 & Spring Boot 深度

1. JVM 内存结构

Heap(堆)
Young Generation
Eden Space <- 新对象分配在这里
Survivor S0/S1 <- 经历多次 GC 的对象
Old Generation <- 长期存活的对象

Non-Heap
Metaspace <- 类元数据(JDK 8+ 替换 PermGen)
Code Cache <- JIT 编译的机器码

Stack(每个线程一个)
方法调用栈帧:局部变量、返回地址、操数栈

2. GC 算法

收集器算法适用场景
Serial GC单线程标记-清除单核/内存小
Parallel GC多线程吸层吸层优先
CMS并发标记-清除低延迟(已废弃)
G1 GC分区 RegionJDK 9+ 默认
ZGC全并发、每次 < 1ms STW超大堆/低延迟

G1 GC 工作原理

  • 将堆划分为大小相等的 Region(2MB)
  • 优先回收垃圾最多的 Region(Garbage First)
  • 可预测停顿时间,默认目标 200ms

3. Java 并发深度

volatile 关键字

private volatile boolean flag = false;

// 保证可见性:一个线程修改后其他线程立即可见
// 保证有序性:Happens-Before 规则
// 不保证原子性:count++ 不是原子操作

synchronized vs ReentrantLock

// synchronized:简单、JVM 自动管理
synchronized (lock) {
// 临界区
}

// ReentrantLock:可中断、公平锁、多条件
ReentrantLock lock = new ReentrantLock(true); // 公平锁
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
// 临界区
}
} finally {
lock.unlock(); // 必须手动释放
}

// 多条件锁实现生产者消费者
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

ConcurrentHashMap 实现(JDK 8+)

  • 数组 + 链表 + 红黑树
  • 不再使用分段锁,转而用 CAS + synchronized
  • 每个桶用 synchronized 魔谁头节点
  • 读操作岆负极小,岅不锁

4. Spring Boot 核心

Bean 生命周期

容器启动
-> 扫描 @Component/@Service/@Repository
-> BeanDefinition 注册
-> 实例化(构造器注入)
-> 属性注入(@Autowired)
-> Aware 接口回调
-> BeanPostProcessor#beforeInit
-> @PostConstruct
-> InitializingBean#afterPropertiesSet
-> BeanPostProcessor#afterInit’s
-> [Bean 处于就绪状态]
-> @PreDestroy -> DisposableBean#destroy

Spring AOP 实现

@Aspect
@Component
public class TimingAspect {
@Around("@annotation(Timed)")
public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long elapsed = System.currentTimeMillis() - start;
log.info("{} took {}ms", pjp.getSignature(), elapsed);
}
}
}

// 实现数据库事务也是通过 AOP 注入事务逻辑
@Transactional // 内部用 CGLIB 实现

@Transactional 常见坏呓

// 1. 同一类内方法自调失效(代理类内部调用)
@Service
public class OrderService {
@Transactional
public void createOrder() {
this.processPayment(); // 事务失效!
}
@Transactional
public void processPayment() { ... }
}

// 解决:注入自身代理
ApplicationContext.getBean(OrderService.class).processPayment();

// 2. 异常类型错误导致不回滚
@Transactional // 默认只回滚 RuntimeException
public void method() throws Exception {
// Exception 不会回滚
}
// 解决
@Transactional(rollbackFor = Exception.class)

5. Spring Cloud 微服务

服务注册: Nacos / Eureka
负载均衡: Spring Cloud LoadBalancer
API 网关: Spring Cloud Gateway
服务调用: OpenFeign
熔断: Resilience4j 
配置中心: Nacos Config
链路追踪: SkyWalking / Micrometer Tracing

OpenFeign 示例

@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
@GetMapping("/users/{id}")
UserDTO getUser(@PathVariable String id);
}

@Component
class UserServiceFallback implements UserServiceClient {
@Override
public UserDTO getUser(String id) {
return UserDTO.empty(); // 降级处理
}
}

6. 性能调优

# JVM 参数调优
java -Xms2g -Xmx2g # 初始和最大堆内存
java -XX:+UseG1GC # 使用 G1 GC
java -XX:MaxGCPauseMillis=200 # GC 停顿目标

# 性能分析工具
arthas # 阿里巴巴开源,线上诊断
jvisualvm / JFR # JVM 性能图谱

# 情境问题快速定位
jstack <pid> # 查看线程堵堵
jmap -heap <pid> # 堆内存分布

7. 资深面试题

  • HashMap 为什么容量是 2 的幂次?
    • 取模可用位运算代替:hash & (n-1) 效率高于 hash % n
    • 2 的幂次时 n-1 的二进制全是 1,哈希分布更均匀
  • Spring 的三级缓存如何解决循环依赖?
    • 一级缓存:完整对象
    • 二级缓存:早期暴露的对象(尚未设置属性)
    • 三级缓存:ObjectFactory(创建对象的工厂)
    • 构造器注入的循环依赖不能解决
  • @Transactional 与 ThreadLocal 的关系?
    • Spring 事务的连接和事务状态绑定在 ThreadLocal 中
    • 异步线程中事务无法传播,需特殊处理
  • JVM 调优第一步看什么?
    • 先确认是 CPU、内存还是 GC 问题
    • GC 日志:分析停顿时间和频率
    • Arthas:在线分析热点方法和内存异常