← 返回面试目录

🔧 后端 Debug 实战指南

真实场景 · 排查流程 · 命令速查 · 思维模型 · 手把手排查

Debug思维 日志排查 接口问题 线上故障 性能调优

Debug 通用思维模型

不管什么问题,都走这个流程。养成习惯之后,排查速度会快 3~5 倍。

1复现问题
确认环境、参数、条件
2缩小范围
网络?服务?数据库?缓存?
3提出假设
最可能的原因是什么?
4验证假设
查日志、加断点、看监控
5修复验证
修复后测试 + 回归
6复盘记录
写WIKI,下次秒定位

Debug 就像医生看病:先问症状(复现)→ 做检查(缩小范围)→ 初步诊断(假设)→ 开药(修复)→ 复查(验证)。不要上来就改代码猜答案!

缩小范围的"二分法"

碰到复杂问题,用二分法快速缩小范围:

问题现象
前端还是后端?
看Network面板

如果是后端 → 服务层还是数据库层?(看接口RT)

如果是数据库 → 慢查询还是锁等待?(看慢查询日志)

如果是服务层 → 代码逻辑还是外部依赖?(看日志+链路追踪)

经验之谈:80% 的线上问题能在 10 分钟内定位,关键是问对问题。"是网络问题吗?是代码问题吗?是数据问题吗?"——每次只排除一半。

日志排查 5 步法

第 1 步:找到正确的日志

# 看应用日志(最常见的入口)
tail -f /var/log/app/application.log

# 搜索关键词
grep -n "ERROR" /var/log/app/application.log | tail -50
grep -n "orderId-12345" /var/log/app/*.log

# 按时间段搜索
sed -n '/2025-06-10 14:30/,/2025-06-10 14:35/p' application.log

# 看 Nginx 访问日志
tail -f /var/log/nginx/access.log | grep "POST /api/order"

第 2 步:看异常堆栈

找到 ERROR 日志后,不要只看第一行!要看完整的异常链(Caused by):

# 常见异常链结构(从下往上看!)
java.lang.NullPointerException
  at com.example.service.OrderService.createOrder(OrderService.java:87)
  at com.example.controller.OrderController.post(OrderController.java:45)

# ⬆️ 关键信息:哪个类、哪个方法、第几行
# ⬆️ 直接定位到 OrderService.java 的第 87 行

第 3 步:追踪请求链路

一个请求可能经过多个服务,用 traceId 串联:

# 用 traceId 追踪完整链路
grep "traceId=abc123" /var/log/app/*.log

# 输出(按时间排列):
14:30:01 [gateway] 收到请求 traceId=abc123 path=/api/order
14:30:01 [order-svc] 创建订单开始 traceId=abc123
14:30:02 [order-svc] 调用库存服务 traceId=abc123
14:30:05 [order-svc] 库存服务超时! traceId=abc123
14:30:05 [order-svc] 订单创建失败 traceId=abc123

没有 traceId?看请求的入参(userId、orderId)一样可以串起来。

第 4 步:关联时间线

把多条日志按时间排列,看先发生了什么

# 按时间排序多个服务的日志
sort -m -t' ' -k1,2 \
  <(sed 's/^/[GATEWAY] /' /var/log/gateway.log) \
  <(sed 's/^/[ORDER] /' /var/log/order.log) \
  <(sed 's/^/[STOCK] /' /var/log/stock.log)

第 5 步:确认根因 vs 表象

很多时候你看到的错误只是表象,要往上游找:

用户报错
"下单失败"
订单服务报错
"调用库存超时"
库存服务
数据库慢查询
根因
缺索引!

不要在"下单失败"这一层修 bug,要追到根因!

接口问题排查

场景 1:接口返回 500

接口500
看应用日志
找Exception
分类型

NullPointerException → 查空值来源,判空处理
IllegalArgumentException → 参数校验问题,看入参
SQLException → SQL 语法/连接池满/死锁
TimeoutException → 下游服务/数据库/Redis 超时
OutOfMemoryError → 内存问题(见 OOM 章节)

场景 2:接口很慢(RT 高)

排查流程

接口 RT > 3s
确认是偶尔慢还是一直慢?
偶尔慢 → 看GC日志
可能是 Full GC 停顿
|
一直慢 → 加耗时日志
打印每一步的耗时
// 在代码里加耗时日志(临时排查用)
long t1 = System.currentTimeMillis();
User user = userDao.findById(userId);
log.info("DB查询耗时: {}ms", System.currentTimeMillis() - t1);

long t2 = System.currentTimeMillis();
Order order = redisClient.get(orderKey);
log.info("Redis查询耗时: {}ms", System.currentTimeMillis() - t2);

场景 3:接口返回数据不对

• 检查入参:前端传的参数对不对?(看 Network 面板)
• 检查逻辑:代码有没有处理边界情况(null、空字符串、0)
• 检查数据:数据库的数据是不是预期的?(直接 SQL 查)
• 检查缓存:是不是读了旧缓存?(清缓存试试

数据库问题排查

慢查询排查流程

接口慢
查慢查询日志
EXPLAIN
加索引/改SQL
# 1. 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; # 超过1秒记录

# 2. 查看最近的慢查询
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;

# 3. EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID';

# 重点关注:
# type=ALL → 全表扫描,需要加索引
# rows很大 → 扫描行数多
# Extra=Using filesort → 额外排序

场景:死锁排查

# 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;

# 在输出中找 "LATEST DETECTED DEADLOCK" 部分
# 会显示:
# Transaction 1: 持有锁A,等待锁B
# Transaction 2: 持有锁B,等待锁A

# 解决方案:
# 1. 统一加锁顺序(都先锁A再锁B)
# 2. 缩小事务范围
# 3. 添加重试机制

场景:连接池满

# 查看当前连接数
SHOW PROCESSLIST;

# 查看最大连接数
SHOW VARIABLES LIKE 'max_connections';

# 排查:
# 1. 连接泄漏?代码里没有 close()
# 2. 慢查询占连接?先杀掉慢查询
# 3. 连接池配置太小?调大 maxPoolSize

# 临时处理:杀掉空闲连接
KILL <thread_id>;

OOM / CPU 飙高排查

CPU 飙高排查流程

CPU 100%
top 找到高CPU进程
top -Hp 找到高CPU线程
printf '%x' 线程ID转16进制
jstack 看线程堆栈
定位到具体代码行
# Step 1: 找高 CPU 进程
top -c
# 假设找到 java 进程 PID = 12345

# Step 2: 找高 CPU 线程
top -Hp 12345
# 假设找到线程 TID = 12350

# Step 3: 转十六进制
printf '%x' 12350
# 输出: 303e

# Step 4: 看线程堆栈
jstack 12345 | grep "303e" -A 30

# 常见原因:
# 1. 死循环(while(true) 没退出条件)
# 2. 正则回溯(恶意输入触发 catastrophic backtracking)
# 3. 频繁 Full GC(jstat -gcutil 观察)
# 4. 加密/序列化等 CPU 密集操作

OOM 排查流程

OOM
导出堆dump
MAT分析
找到泄漏对象
# 1. 启动时加参数,OOM自动导出dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof

# 2. 手动导出(进程还活着的话)
jmap -dump:format=b,file=/tmp/heap.hprof 12345

# 3. 用 MAT (Memory Analyzer Tool) 打开分析
# 看 "Leak Suspects" 和 "Dominator Tree"

# 常见 OOM 原因:
# 1. ThreadLocal 未 remove → 内存泄漏
# 2. 集合类只 add 不 remove → 无限增长
# 3. 大对象一次查太多 → SQL 限流
# 4. 动态代理/CGLIB 生成类过多 → Metaspace OOM

紧急处理:线上 OOM 或 CPU 100% 时,先导出 dump 再重启!重启后现场就没了。用 jmap -dump 先把堆存下来,然后重启恢复服务,再慢慢分析 dump 文件。

并发 Bug 排查

并发问题的 5 个信号

遇到以下情况,第一反应要想到"可能是并发问题":

偶发性 Bug:本地永远复现不了,线上偶尔出现
数据不一致:两个请求同时操作,结果不对
重复执行:用户双击提交,订单创建了两个
计数不准:并发下 i++ 结果偏小
死锁:线程卡住不动,CPU 接近 0

场景:重复下单

现象:用户快速点两次"提交",创建了两个订单。

原因:前端没防重复提交 + 后端没做幂等。

解决方案:

• 前端:提交按钮加 loading,防双击
• 后端:用 Token 机制唯一索引(orderId + userId 联合唯一)
• 后端:用 Redis setnx 做防重(key = "order:"+userId+":"+hash,TTL=5s)

场景:超卖问题

现象:库存 10 件,卖了 12 件。

原因:读库存和扣库存之间没有加锁。

解决方案(从简单到复杂):

数据库乐观锁:UPDATE stock SET count=count-1 WHERE id=1 AND count>0
Redis 原子操作:DECR key(扣到 0 以下就拒绝)
分布式锁:Redisson lock + 扣库存
消息队列:库存扣减请求串行化

Redis 问题排查

Redis 常见问题速查

问题现象排查命令常见原因
Redis 慢 GET/SET 耗时高 SLOWLOG GET 10 BigKey、复杂命令、内存满
连接超时 Connection refused redis-cli INFO clients maxclients 达上限、网络问题
内存满 写入报 OOM INFO memory 没设过期、BigKey、淘汰策略不对
数据丢失 重启后数据没了 CONFIG GET save RDB 没保存 / AOF 未开启
缓存雪崩 大量请求打到DB 看DB连接数飙升 大量 key 同时过期
# Redis 日常排查命令
redis-cli

# 查看内存使用
INFO memory

# 查看慢日志
SLOWLOG GET 10

# 查看连接数
INFO clients

# 查看大Key(慎重!生产环境用 scan 代替 keys)
redis-cli --bigkeys

# 查看某个 key 的内存占用
MEMORY USAGE mykey

排查 Checklist(照着走)

接到线上报警后,5 分钟内完成:

确认报警真实性(是误报还是真问题)

确认影响范围(影响多少用户?哪个功能?)

看最近的部署/变更记录("刚才发版了吗?")

看应用 ERROR 日志(最近的异常)

看监控大盘(CPU/内存/QPS/RT)

接口报错排查 Checklist:

看 HTTP 状态码(4xx 参数问题 / 5xx 服务端问题)

看 Response Body(有没有具体错误信息)

看应用日志(grep 请求ID或关键词)

看上游/下游服务日志(是不是依赖方的问题)

看数据库/Redis(是不是存储层的问题)

看网络(ping / telnet 测试连通性)

性能问题排查 Checklist:

确认是"一直慢"还是"偶尔慢"

看接口 RT 分布(P50/P95/P99)

看慢查询日志

看 GC 日志(是不是 Full GC 停顿)

看线程池状态(是不是线程池满了排队)

看 Redis 响应时间

看网络延迟(ping / traceroute)

最重要的一条经验:遇到问题先别急着改代码!花 5 分钟完整描述问题(什么时间、什么操作、什么现象、什么环境),80% 的 bug 在描述的过程中就能猜到原因。