Production Troubleshooting
线上排查场景集 6个生产环境实战故事
真实的生产故障,完整的排查过程。从告警到定位到修复,每一步都有真实的命令、日志和代码。收藏它,下次线上出问题时不慌。
📝 6 个实战场景
🐛 CPU / 内存 / 死锁 / 超时 / MQ / 回滚
🔧 完整排查流程
⏱️ 约 45 分钟阅读
🔥
Story 1: CPU 飙到 100%
下午3点,线上突然所有接口都超时了。用户疯狂投诉...
生产故障 · P0 级
2026-06-10 15:00 — 接口大面积超时
1.1 收到告警
钉钉群里突然弹出一条告警消息:
🚨
CPU告警:服务器 192.168.1.100 CPU使用率 99.2%,持续时间 5分钟
机器人
监控机器人
🚨 CPU 告警
服务器: 192.168.1.100
CPU使用率: 99.2%
持续时间: 5 分钟
告警级别: P0 - 紧急
1.2 SSH 登服务器
第一时间 SSH 到告警服务器,用 top 看看谁在吃 CPU。
Terminal — ssh ops@192.168.1.100
$ ssh ops@192.168.1.100
Welcome to Ubuntu 22.04 LTS
$ top
top - 15:05:33 up 45 days, 3 users, load average: 32.15, 28.43, 15.21
PID USER %CPU %MEM COMMAND
8923 java 99.2 23.1 java -jar app.jar ← 这个进程吃了99%!
1234 mysql 3.1 45.2 mysqld
5678 nginx 0.5 1.2 nginx: worker
Java进程吃了99%的CPU!找到PID了:8923。接下来看是哪个线程在作怪。
1.3 看线程 — 哪个线程吃的最多?
用 top -Hp 可以看到进程里的每个线程。
$ top -Hp 8923
top - 15:06:12 up 45 days, load average: 32.15, 28.43, 15.21
PID USER %CPU COMMAND
9012 java 85.3 ← 这个线程吃的最多!
9013 java 3.1
9014 java 2.8
9015 java 1.2
8923 java 0.8
线程 PID 9012 吃了 85% 的 CPU。现在需要知道它在 Java 里对应什么。
1.4 jstack 线程 dump — 定位到代码行
Java 线程 ID 是十进制的,jstack 里是十六进制。先转换,再 grep。
$ jstack 8923 > thread_dump.txt
Heap dump created.
$ printf "%x\n" 9012
2324
$ grep -A 30 "2324" thread_dump.txt
"pool-3-thread-1" #45 prio=5 os_prio=0 tid=0x00007f8c34123450 nid=0x2324 runnable
at com.energy.service.AlertCheckService.checkLoop(AlertCheckService.java:67)
at com.energy.service.AlertCheckService.run(AlertCheckService.java:45)
at java.lang.Thread.run(Thread.java:748)
找到了!AlertCheckService 第67行,一个 while(true) 循环没有 sleep!
1.5 看代码 — 死循环!
打开 IntelliJ,定位到 AlertCheckService.java:
IntelliJ IDEA — AlertCheckService.java
AlertCheckService.java
AlertMapper.java
📂 com.energy
📂 service
📄 AlertCheckService.java
📄 AlertMapper.java
📂 model
📄 Alert.java
60 public void checkLoop () {
61 while (running) {
62 // 没有 sleep!死循环!
63 List<Alert> alerts = alertMapper.findPending ();
64 for (Alert alert : alerts) {
65 processAlert (alert);
66 }
67 // 应该在这里加 Thread.sleep(5000);
68 }
69 }
这个 while(running) 循环里没有任何等待,每秒执行几万次数据库查询。数据库返回空结果也要不了多久,但循环本身不停运转,CPU 就 100% 了。
1.6 修复 — 加上 sleep
在循环里加上 sleep,每5秒检查一次:
IntelliJ IDEA — AlertCheckService.java (修复后)
📂 com.energy
📂 service
📄 AlertCheckService.java
60 public void checkLoop () {
61 while (running) {
62 List<Alert> alerts = alertMapper.findPending ();
63 for (Alert alert : alerts) {
64 processAlert (alert);
65 }
66 try {
67 Thread.sleep (5000 ); // 5秒检查一次
68 } catch (InterruptedException e) {
69 Thread.currentThread ().interrupt ();
70 break ;
71 }
72 }
73 }
diff --git a/AlertCheckService.java b/AlertCheckService.java
@@ -64,6 +64,11 @@
}
+ try {
+ Thread.sleep(5000); // 5秒检查一次
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
}
}
1.7 验证 — CPU 下来了
发版后验证:
Terminal — top -Hp 8923 (修复后)
$ top -Hp 8923
top - 15:45:12 up 45 days, load average: 2.31, 3.45, 4.12
PID USER %CPU %MEM COMMAND
8923 java 3.2 23.1 java -jar app.jar ← CPU下来了!
9012 java 1.8
9013 java 0.5
15:05
SSH登服务器,top 定位到 Java 进程 PID 8923
15:08
top -Hp 找到线程 PID 9012(85.3% CPU)
15:12
jstack + printf "%x" 定位到 AlertCheckService.java:67
15:20
加 Thread.sleep(5000),发版验证,CPU 降到 3.2%
💾
Story 2: 内存泄漏
服务运行3天后越来越慢,GC也回收不了。每次重启只管3天...
性能劣化 · P1 级
2026-06-10 10:00 — 服务又变慢了(第3天)
📈 堆内存增长曲线 — 内存泄漏的"温水煮青蛙"
💡 内存只涨不降,Full GC 也回收不了 → 典型内存泄漏特征。ThreadLocal 未清理导致对象一直被引用。
2.1 现象 — GC越来越频繁,回收不了
服务每次刚启动很快,但运行3天后就越来越慢。看 GC 日志:
$ tail -100 /var/log/app/gc.log
[GC (Allocation Failure) [PSYoungGen: 655360K->655358K(655360K)] 1560120K->1560118K(2097152K), 0.9876543 secs]
[Full GC (Ergonomics) [PSYoungGen: 655358K->655358K(655360K)] 1560118K->1560118K(2097152K), 4.3210987 secs]
[GC (Allocation Failure) [PSYoungGen: 655360K->655356K(655360K)] 1560116K->1560114K(2097152K), 0.8765432 secs]
[Full GC (Ergonomics) [PSYoungGen: 655356K->655356K(655360K)] 1560114K->1560114K(2097152K), 5.1234567 secs]
# Full GC 越来越频繁,但回收不了任何内存!
# Young GC 后内存几乎没变:655360K -> 655358K
# Full GC 也回收不了:1560118K -> 1560118K
Full GC 越来越频繁,而且回收不了什么内存。这是内存泄漏的典型特征——对象被引用着,GC无法回收。
2.2 jmap dump — 堆快照
先用 jmap 把堆内存 dump 下来:
Terminal — jmap heap dump
$ jmap -dump:format=b,file=heap.hprof 8923
Dumping heap to /tmp/heap.hprof ...
Heap dump file created (823456789 bytes)
# 文件 785MB,用 MAT (Memory Analyzer Tool) 分析
# 下载到本地:scp ops@192.168.1.100:/tmp/heap.hprof ./
2.3 MAT 分析 — 找到泄漏嫌疑人
用 Eclipse MAT 打开 heap.hprof,查看 Leak Suspects:
Eclipse MAT — Leak Suspects Report
🔥 Problem Suspect 1:
java.util.concurrent.ConcurrentHashMap @ 0x7f8c34123450
├─ 856,321 items ← 这个Map无限增长!
├─ Retained Heap: 456 MB
├─ Key: com.energy.cache.DeviceCacheEntry
└─ At com.energy.service.DeviceCacheManager.deviceMap (DeviceCacheManager.java:23)
✅ Problem Suspect 2 (minor):
java.util.ArrayList @ 0x7f8c34098765
├─ 12,345 items
├─ Retained Heap: 23 MB
🔥
ConcurrentHashMap 有 856,321 个 Entry,占 456MB!而且一直在增长,永远不清理。
2.4 看代码 — 只加不删的 Map
打开 DeviceCacheManager.java:
IntelliJ IDEA — DeviceCacheManager.java
DeviceCacheManager.java
DeviceCacheEntry.java
📂 com.energy
📂 cache
📄 DeviceCacheManager.java
📄 DeviceCacheEntry.java
18 @Component
19 public class DeviceCacheManager {
20
21 // 问题:只往里加,从来不清理!
22 private Map<String, DeviceCacheEntry> deviceMap =
23 new ConcurrentHashMap<>();
24
25 public void updateCache (String deviceId, DeviceData data) {
26 deviceMap.put (deviceId, new DeviceCacheEntry(data));
27 }
28 // 没有 remove/evict 方法!
29 }
找到了!ConcurrentHashMap 只 put 不 remove。每次有设备数据更新就往里塞,设备下线了也不清理。3天下来,85万个 Entry 堆在内存里。
2.5 修复 — 用 Guava Cache 加过期清理
替换成 Guava Cache,自动过期 + 最大容量限制:
IntelliJ IDEA — DeviceCacheManager.java (修复后)
📂 com.energy
📂 cache
📄 DeviceCacheManager.java
18 @Component
19 public class DeviceCacheManager {
20
21 private static final long EXPIRE_MINUTES = 30 ;
22 private Cache<String, DeviceCacheEntry> deviceCache =
23 CacheBuilder.newBuilder ()
24 .expireAfterWrite (EXPIRE_MINUTES, TimeUnit.MINUTES )
25 .maximumSize (10000 )
26 .build ();
27 }
diff --git a/DeviceCacheManager.java b/DeviceCacheManager.java
@@ -20,8 +20,8 @@
- private Map<String, DeviceCacheEntry> deviceMap =
- new ConcurrentHashMap<>();
+ private static final long EXPIRE_MINUTES = 30;
+ private Cache<String, DeviceCacheEntry> deviceCache =
+ CacheBuilder.newBuilder()
+ .expireAfterWrite(EXPIRE_MINUTES, TimeUnit.MINUTES)
+ .maximumSize(10000)
+ .build();
关键改动:1) 30分钟自动过期,设备下线后缓存会自动清理;2) 最大10000条,防止无限增长;3) Guava Cache 线程安全,不需要 ConcurrentHashMap。
🔒
Story 3: 数据库死锁
几个用户同时操作,业务全部卡住了。数据库报"锁等待超时"。
数据库故障 · P0 级
2026-06-10 11:30 — 死锁告警
🔒 死锁环形等待图 — 循环依赖是死锁的本质
事务 A(placeOrder)
✅ 持有锁: id=1(订单行)
⏳ 等待锁: id=3(库存行)
事务 B(adjustStock)
✅ 持有锁: id=3(库存行)
⏳ 等待锁: id=1(订单行)
⚠️ A 等 B 释放锁,B 等 A 释放锁 → 永远等不到 → MySQL 只能回滚其中一个事务
✅ 解决方案:统一加锁顺序(都先锁库存再锁订单),就不会出现循环等待
3.1 现象 — 用户反馈操作失败
客服
客服小李
好几个用户反馈操作失败,提示"锁等待超时" Lock wait timeout exceeded
运维
运维老赵
我看了数据库,有死锁!MySQL 自动检测到了
我
我(开发)
我来看看 INNODB STATUS,查死锁详情
3.2 查死锁 — INNODB STATUS
在 DataGrip 里执行 SHOW ENGINE INNODB STATUS:
DataGrip — 192.168.1.100/energy_db
经典死锁!事务1锁了订单等库存,事务2锁了库存等订单。互相等待,MySQL 只能回滚其中一个。
死锁的本质是"循环等待"。A等B,B等A。解决方案:让所有事务按相同顺序加锁,就不会出现循环等待。
3.3 看代码 — 加锁顺序不一致
两个方法操作同样的表,但加锁顺序不同:
IntelliJ IDEA — OrderService.java
OrderService.java
StockService.java
📂 com.energy
📂 service
📄 OrderService.java
📄 StockService.java
30 // 方法1:先锁订单,再锁库存
31 @Transactional
32 public void placeOrder (Long orderId, Long stockId) {
33 orderMapper.selectForUpdate (orderId); // 锁订单
34 stockMapper.updateStock (stockId); // 锁库存
35 }
36
37 // 方法2:先锁库存,再锁订单(反了!)
38 @Transactional
39 public void adjustStock (Long stockId, Long orderId) {
40 stockMapper.updateStock (stockId); // 锁库存
41 orderMapper.selectForUpdate (orderId); // 锁订单
42 }
placeOrder 先锁订单再锁库存,adjustStock 先锁库存再锁订单——顺序反了,死锁!
3.4 修复 — 统一加锁顺序
原则:所有事务都按相同顺序加锁,就不会死锁。 规定统一先锁库存再锁订单:
IntelliJ IDEA — OrderService.java (修复后)
📂 com.energy
📂 service
📄 OrderService.java
30 // 修复前:先锁订单再锁库存
31 // @Transactional
32 // public void placeOrder(Long orderId, Long stockId) {
33 // orderMapper.selectForUpdate(orderId); // 锁订单
34 // stockMapper.updateStock(stockId); // 锁库存
35 // }
36
37 // 修复后:统一先锁库存再锁订单
38 @Transactional
39 public void placeOrder (Long orderId, Long stockId) {
40 stockMapper.updateStock (stockId); // 先锁库存
41 orderMapper.selectForUpdate (orderId); // 再锁订单
42 }
diff --git a/OrderService.java b/OrderService.java
@@ -30,7 +30,7 @@
@Transactional
public void placeOrder(Long orderId, Long stockId) {
- orderMapper.selectForUpdate(orderId); // 锁订单
- stockMapper.updateStock(stockId); // 锁库存
+ stockMapper.updateStock(stockId); // 先锁库存
+ orderMapper.selectForUpdate(orderId); // 再锁订单
}
11:35
SHOW ENGINE INNODB STATUS 发现死锁:T1等T2,T2等T1
11:50
统一加锁顺序:先库存后订单,发版后死锁消失
⏱️
Story 4: 接口偶发超时
不是每次都慢,但偶尔就超时。排查了好几天,最终发现是缓存击穿。
性能问题 · P1 级
2026-06-10 09:00 — 又有用户反馈超时
4.1 现象 — P99 偶尔超慢
看 APM 监控,大部分请求都很快,但偶尔有超慢的:
APM 监控 — /api/devices 响应时间
P50 只要 120ms,但 P99 要 5.2 秒。说明大部分请求都很快,偶尔有几个特别慢。
4.2 排查过程 — 一步步缩小范围
1. 看应用日志
→
2. 看数据库慢查询
→
3. 发现规律
→
4. 定位缓存
Step 1
看应用日志 → 慢的请求没有报错,正常返回只是耗时长
Step 2
看数据库慢查询日志 → 偶尔出现慢查询,查询本身没优化空间
Step 3
发现规律 → 慢查询都发生在同一个时间点,而且都查同一张表
Step 4
联想到缓存 → 是不是缓存失效的瞬间,请求全打到数据库了?
查 Redis 缓存命中率:
$ redis-cli info stats | grep keyspace
keyspace_hits:123456
keyspace_misses:8765
# 命中率 = 123456 / (123456 + 8765) = 93.4%
# 7% 的请求打到数据库了!
缓存命中率只有 93%,7% 的请求打到数据库了。看看到底是什么 key 失效。
4.3 发现缓存击穿
用 redis-cli monitor 观察,发现热点 key 过期时,大量请求同时打到数据库:
Terminal — redis-cli monitor
$ redis-cli monitor | grep "device:detail:1001"
1718000000.123 [0 10.0.0.5:54321] "get" "device:detail:1001" ← 缓存命中
1718000000.156 [0 10.0.0.5:54322] "get" "device:detail:1001" ← 缓存命中
1718000030.001 [0 10.0.0.5:54321] "get" "device:detail:1001" ← 过期了!miss
1718000030.003 [0 10.0.0.5:54323] "get" "device:detail:1001" ← miss
1718000030.005 [0 10.0.0.5:54324] "get" "device:detail:1001" ← miss
1718000030.007 [0 10.0.0.5:54325] "get" "device:detail:1001" ← miss
1718000030.009 [0 10.0.0.5:54326] "get" "device:detail:1001" ← miss
# 某个热点 key 过期了,大量请求同时打到 DB!
缓存击穿 :热点 key 过期的瞬间,几百个并发请求同时发现缓存没了,全部去查数据库,数据库瞬间被打爆。这就是 P99 飙到 5 秒的原因!
4.4 修复 — 互斥锁重建缓存
只让一个请求去查数据库并重建缓存,其他请求等一下再重试:
IntelliJ IDEA — DeviceService.java (修复后)
📂 com.energy
📂 service
📄 DeviceService.java
50 public DeviceVO getDeviceDetail (Long id) {
51 String key = "device:detail:" + id;
52 String cached = redisTemplate.opsForValue().get (key);
53 if (cached != null )
54 return JSON.parseObject (cached, DeviceVO.class );
55
56 // 互斥锁:只让一个请求去查DB
57 String lockKey = "lock:" + key;
58 if (redisTemplate.opsForValue().setIfAbsent (
59 lockKey, "1" , 10 , TimeUnit.SECONDS )) {
60 try {
61 DeviceVO result = deviceMapper.selectDetail (id);
62 redisTemplate.opsForValue().set (key,
63 JSON.toJSONString (result),
64 30 , TimeUnit.MINUTES );
65 return result;
66 } finally {
67 redisTemplate.delete (lockKey);
68 }
69 }
70 // 其他请求等一下重试
71 Thread.sleep (100 );
72 return getDeviceDetail (id);
73 }
setIfAbsent 就是 Redis 的 SETNX 命令——只有第一个请求能获得锁去查 DB,其他请求 sleep 100ms 后重试(此时缓存已经重建好了)。
📨
Story 5: 消息队列积压
RabbitMQ 消息堆积 50 万条,消费者处理不过来。订单一直在排队...
消息积压 · P1 级
2026-06-10 14:00 — MQ 监控告警
5.1 发现问题 — 消息堆积50万
打开 RabbitMQ Management UI,看到 order.process 队列的消息数:
RabbitMQ Management — Queues
Queue: order.process
ALERT
⚠️ 生产速度 (500/s) 远大于消费速度 (80/s)!消息一直在堆积!
消费速度 80/s 远远跟不上生产速度 500/s。消息越积越多,6小时了还没消化完。
5.2 查消费者日志 — 处理太慢
为什么消费速度只有 80/s?看消费者日志:
$ grep -i "slow" /var/log/app/app.log | tail -5
2026-03-20 14:30:22 WARN OrderConsumer - Processing took 12.5s for order ORD20260320001
2026-03-20 14:30:35 WARN OrderConsumer - Processing took 15.2s for order ORD20260320002
2026-03-20 14:30:50 WARN OrderConsumer - Processing took 11.8s for order ORD20260320003
2026-03-20 14:31:05 WARN OrderConsumer - Processing took 14.6s for order ORD20260320004
2026-03-20 14:31:18 WARN OrderConsumer - Processing took 13.1s for order ORD20260320005
每条消息处理要 12-15 秒!正常应该 100ms 以内。2个消费者,每秒只能处理约 80/12 ≈ 6.6 条。看代码。
5.3 看消费者代码 — 同步调外部 API
消费者里每条消息都同步调用一个外部 API,这个 API 很慢:
IntelliJ IDEA — OrderConsumer.java
📂 com.energy
📂 consumer
📄 OrderConsumer.java
25 @RabbitListener (queues = "order.process" )
26 public void onMessage (OrderMessage msg) {
27 // 同步调用外部物流API — 每次要10-15秒!
28 LogisticsResult result = logisticsClient.createOrder (msg);
29 orderService.updateLogistics (msg.getOrderId(), result);
30 }
logisticsClient.createOrder() 是同步调用,外部 API 响应慢,每条消息要等 12-15 秒。消费者就这样被阻塞了。
5.4 临时解决 — 增加消费者
先紧急增加消费者实例,提高消费速度:
$ docker-compose up -d --scale consumer=5
Creating consumer_3 ... done
Creating consumer_4 ... done
Creating consumer_5 ... done
# 消费速度从 80/s 提升到约 200/s
# 但还是不够!根本问题:每条消息太慢
加消费者只是临时方案,治标不治本。根本解决要把同步调用改成异步,或者做批量处理。
5.5 永久修复 — 异步化 + 批量处理
把同步调用改成异步,消费者只做消息接收和入库,物流 API 调用改为异步:
IntelliJ IDEA — OrderConsumer.java (修复后)
📂 com.energy
📂 consumer
📄 OrderConsumer.java
25 @RabbitListener (queues = "order.process" )
26 public void onMessage (OrderMessage msg) {
27 // 1. 先入库,状态设为"待处理"
28 orderService.savePending (msg);
29 // 2. 异步调用物流API(不阻塞消费者线程)
30 CompletableFuture.runAsync (() -> {
31 LogisticsResult result = logisticsClient.createOrder (msg);
32 orderService.updateLogistics (msg.getOrderId(), result);
33 }, asyncExecutor);
34 }
diff --git a/OrderConsumer.java b/OrderConsumer.java
@@ -25,6 +25,10 @@
@RabbitListener(queues = "order.process")
public void onMessage(OrderMessage msg) {
- LogisticsResult result = logisticsClient.createOrder(msg);
- orderService.updateLogistics(msg.getOrderId(), result);
+ // 1. 先入库,状态设为"待处理"
+ orderService.savePending(msg);
+ // 2. 异步调用物流API
+ CompletableFuture.runAsync(() -> {
+ LogisticsResult result = logisticsClient.createOrder(msg);
+ orderService.updateLogistics(msg.getOrderId(), result);
+ }, asyncExecutor);
}
消费者只做入库(很快),物流API调用在独立线程池异步执行。消费速度从 80/s 提升到 3,200/s!
⏪
Story 6: 发布出问题→紧急回滚
新版本上线5分钟就发现数据错乱,紧急回滚!
线上故障 · P0 级
2026-06-10 16:00 — 新版本发布
6.1 发布 — v1.2.3 上线
下午4点,按计划发布新版本 v1.2.3:
$ docker-compose pull app
Pulling app (myshop-app:v1.2.3)...
Downloaded newer image for myshop-app:v1.2.3
$ docker-compose up -d app
Recreating myshop_app_1 ... done
$ curl -s http://localhost:8080/actuator/health
{"status":"UP","version":"1.2.3"}
# 发布完成!看起来一切正常...
6.2 发现问题 — 5分钟后
客服
客服小王 (2分钟后)
有客户说看到的订单数据不对,金额全变成0了!
测试
测试小李
我试了一下,确认金额显示0。订单列表接口返回的 amount 字段都是 0!
⚠️
线上数据异常!金额全变0,影响所有用户。优先回滚恢复服务,再排查原因!
6.3 紧急回滚 — 回到 v1.2.2
# 查看历史版本
$ docker images | grep myshop
myshop-app v1.2.3 a1b2c3d4e5f6 2 hours ago
myshop-app v1.2.2 f6e5d4c3b2a1 Yesterday ← 回滚到这个
myshop-app v1.2.1 e5d4c3b2a1f6 3 days ago
# 回滚
$ docker-compose down app
Stopping myshop_app_1 ... done
$ docker tag myshop-app:v1.2.2 myshop-app:latest
$ docker-compose up -d app
Creating myshop_app_1 ... done
回滚完成!从发现问题到回滚完成,只用了8分钟。关键:平时就保留历史版本镜像,回滚时才快。
6.4 验证回滚
$ curl -s http://localhost:8080/actuator/health
{"status":"UP","version":"1.2.2"}
$ curl -s http://localhost:8080/api/orders/1001 | jq '.amount'
128.50
# 金额回来了!不是0了!服务恢复正常。
6.5 排查原因 — 哪个 commit 引入的 bug?
回滚后用户恢复了,但得找到 bug 根因,否则下次还会出问题。
$ git log --oneline v1.2.2..v1.2.3
a1b2c3d fix: order amount display issue
e5f4g3h feat: add order discount
c7d8e9f refactor: order DTO mapping
# c7d8e9f 看起来是重构相关的,先看这个
$ git show c7d8e9f
commit c7d8e9f
Author: developer <dev@energy.com>
Date: Wed Jun 10
refactor: order DTO mapping
--- a/OrderDTO.java
- private BigDecimal amount;
+ private BigDecimal discount;
// 字段名改了,但前端还是读 amount!
找到了!c7d8e9f 这个 commit 在重构 DTO 时把 amount 字段改名为 discount,但前端和 API 返回还在读 amount。导致 amount 字段为 null,序列化成 JSON 时变成 0。
VS Code — OrderDTO.java (有 bug 的版本)
public class OrderDTO {
// BUG: amount 被改名为 discount,但前端还读 amount
private BigDecimal amount ;
private BigDecimal discount ; ← 新加的字段
// amount 字段没了!前端读 amount = null → 序列化成 0
}
6.6 修复再发 — 加回 amount 字段
在 OrderDTO 里同时保留 amount 和 discount 两个字段:
VS Code — OrderDTO.java (修复后)
public class OrderDTO {
private BigDecimal amount ; ← 加回来了!
private BigDecimal discount ;
// 金额 = 原价 - 折扣
public BigDecimal getAmount () {
return originalPrice.subtract (discount);
}
}
diff --git a/OrderDTO.java b/OrderDTO.java
@@ -3,6 +3,11 @@
+ private BigDecimal amount;
+ private BigDecimal discount;
+
+ // 金额 = 原价 - 折扣
+ public BigDecimal getAmount() {
+ return originalPrice.subtract(discount);
+ }
}
测试后重新部署:
# 运行测试
$ mvn test -Dtest=OrderDTOTest
Tests run: 5, Failures: 0, Errors: 0
# 构建新版本
$ docker build -t myshop-app:v1.2.4 .
Successfully built a1b2c3d4e5f7
# 部署
$ docker tag myshop-app:v1.2.4 myshop-app:latest
$ docker-compose up -d app
Recreating myshop_app_1 ... done
$ curl -s http://localhost:8080/api/orders/1001 | jq '.amount'
128.50
# 金额正确!v1.2.4 部署成功。
16:08
确认 bug,紧急回滚到 v1.2.2
16:30
git show 找到 bug commit c7d8e9f
17:00
修复代码,测试通过,部署 v1.2.4
📊
排查心法总结
6个实战故事的排查方法论,收藏它,下次出问题不慌
🔥 CPU 飙高
top 找进程 → top -Hp 找线程
jstack + printf "%x" 定位代码行
常见原因:死循环、正则回溯、频繁GC
修复:加 sleep、优化算法
💾 内存泄漏
GC 日志看 Full GC 频率和回收效果
jmap -dump 导出堆快照
MAT 分析 Leak Suspects
常见原因:Map只加不删、监听器未注销
🔒 数据库死锁
SHOW ENGINE INNODB STATUS 查死锁
看两个事务的锁等待关系
常见原因:加锁顺序不一致
修复:统一加锁顺序
⏱️ 偶发超时
APM 看 P99 分布,不要只看平均
查缓存命中率(info stats)
常见原因:缓存击穿/雪崩
修复:互斥锁重建缓存
📨 MQ 积压
MQ Management UI 看消息数和消费速度
查消费者日志,看处理耗时
常见原因:同步调外部API
临时:加消费者;根治:异步化
⏪ 发布回滚
保留历史版本镜像,回滚才快
先回滚恢复,再排查原因
git log 找 bug commit
修复→测试→重新部署
排查通用 Checklist
1. 确认现象 — 看监控、看告警、看用户反馈。确认是"什么"出了问题。
2. 定位范围 — 是前端还是后端?是应用还是数据库?是网络还是服务?
3. 查日志 — 应用日志、GC日志、慢查询日志、Nginx访问日志。
4. 看指标 — CPU、内存、磁盘IO、网络IO、线程数、连接数。
5. 临时止血 — 回滚、扩容、限流、降级。先恢复服务。
6. 根因修复 — 找到代码问题,修复、测试、上线。
常用排查命令速查
# ===== CPU 问题 =====
top # 看整体CPU
top -Hp <PID> # 看进程内线程
jstack <PID> # 线程dump
printf "%x\n" <TID> # 十进制转十六进制
# ===== 内存问题 =====
jmap -dump:format=b,file=heap.hprof <PID> # 堆dump
jstat -gc <PID> 1000 # 实时GC统计
# ===== 数据库问题 =====
SHOW ENGINE INNODB STATUS # 查死锁
SHOW PROCESSLIST # 查当前连接
SELECT * FROM information_schema.INNODB_LOCKS # 查锁
# ===== 缓存问题 =====
redis-cli info stats # 缓存命中率
redis-cli monitor # 实时监控命令
# ===== 部署回滚 =====
docker images | grep <app> # 查历史版本
docker tag <old> <app>:latest # 回滚版本
git log --oneline A..B # 查版本间commit
↑