日期:2026-09-14
总题数:3 题(进阶 2 题 + 高阶 1 题)
方向覆盖:性能优化、线程池优化、架构设计
场景:某 Java 服务 4 核 8G,上线 2 周后 CPU 从 30% 飙到 95%,线程数从 50 涨到 800,响应时间从 50ms 涨到 2s。监控显示:
synchronized 块问题:
分析 GPU 飙高,首先看具体是哪个线程,可以通过 top 去查看具体的线程 ID,明确了 ID 之后可以看出来具体是哪个 ID,然后哪个 ID 导致信息又是过高。然后进一步想看内部细节的话,可以使用 Arris,然后登录它的看板,线程,Thread 去看一下具体是哪些线程导致的当前 CPU 过高占用,然后去明确线程的情况。 线程数上涨,从它的具体情况看,应该是大量的线程被占用了。然后把任务把线程栈打满,然后去扩展到非核心线程数,然后去进行使用,最终导致大量的线程创建。其实这个问题最终还是要确认为什么会有大量的线程超时,响应超时。这个超时的原因具体是因为什么?从当前得到的数据看,超时应该是因为 GC,大量的 GC,FullGC,5 分钟一次,其中堆内存有说 old 占的比例非常高的区,然后伊甸区频繁回收,也就是大量的 YoungGC 产生,这有可能是导致线程数暴涨的一个原因。在每个线程执行任务的时候,都导致了很多的对象创建,最终发生 GC,然后导致整个线程缓慢,然后再创建新的线程,线程伊甸一点点叠加出来,出现当下的情况。然后老年代没有被回收,确认一下是对象泄露了还是什么原因,具体对象是怎么回事?伊甸区频繁回收,也就是年轻代频繁回收的情况下,老年代还很满,也就是说有大量的对象存活了超长的时间。另外最后一点,线程有大量的线程卡在同步那,比如同步块中有东西缓慢,可能是数据库,可能是一些外部调用,可能是一些 IO 情况,最终导致线程的阻塞,导致了当前线程缓慢,其他线程阻塞,最终导致整个服务拖慢,然后创建大量线程去开新的任务,最终导致当下的问题。
优点:
top → Arthas thread 是标准定位路径不足:
代码问题(内存泄漏 + 大对象分配 + 锁竞争)
↓
Young GC 频繁(大量短生命周期对象)
↓
Old 区快速填满(对象过早晋升 + 内存泄漏)
↓
Full GC 频繁(5 分钟一次,STW 1.5s)
↓
线程执行变慢(STW + 锁竞争)
↓
任务堆积 → 线程池创建新线程
↓
线程数从 50 暴涨到 800
↓
CPU 飙高(800 个线程上下文切换 + GC 线程)
↓
响应时间 50ms → 2s
关键洞察:这不是三个独立问题,而是一个根因引发的连锁反应。
Step 1: top - 找到高 CPU 进程
↓
Step 2: top -Hp <pid> - 找到高 CPU 线程 ID
↓
Step 3: printf '%x' <tid> - 转为 16 进制
↓
Step 4: Arthas thread - 分析线程状态
↓
Step 5: Arthas profiler - 定位热点方法
↓
Step 6: Arthas heapdump - 导出内存快照
↓
Step 7: MAT 分析 - 定位内存泄漏
# Step 1: 找到高 CPU 进程
top
# 假设 PID = 12345
# Step 2: 找到高 CPU 线程
top -Hp 12345
# 假设 TID = 12346
# Step 3: 转为 16 进制
printf '%x\n' 12346
# 输出:302a
# Step 4: Arthas 分析线程
thread -n 3 # 最忙的 3 个线程
thread -b # 找出阻塞其他线程的线程
thread 302a # 查看特定线程详情
# Step 5: Arthas profiler(定位热点方法)
profiler start
# 等 30 秒
profiler stop --format html --file /tmp/profile.html
# 打开 profile.html 看火焰图
# Step 6: Arthas heapdump(导出内存快照)
heapdump --live /tmp/heap.hprof
| 指标 | 正常值 | 当前值 | 诊断 |
|---|---|---|---|
| CPU 使用率 | < 70% | 95% | 过高 |
| 线程数 | 50-100 | 800 | 暴涨 |
| Full GC 频率 | < 1 次/天 | 3 次/小时 | 过高 |
| STW 耗时 | < 100ms | 1.5s | 过长 |
| Old 区占用 | < 50% | 90% | 过高 |
| Young GC 频率 | 1-10 次/秒 | 10+ 次/秒 | 过高 |
| BLOCKED 线程 | < 10 | 150 | 锁竞争严重 |
线程池配置:corePoolSize=50, maxPoolSize=200, queue=LinkedBlockingQueue(1000)
↓
GC 频繁 → 线程执行变慢(STW 1.5s + 锁竞争)
↓
任务堆积 → 队列满(1000 个任务)
↓
线程池创建新线程(直到 maxPoolSize=200)
↓
新线程也变慢(GC + 锁竞争)
↓
任务继续堆积 → 超过 maxPoolSize
↓
拒绝策略(AbortPolicy)→ 任务丢失 或 抛出异常
↓
调用方重试 → 更多任务进入
↓
线程池反复创建线程(旧线程超时回收后重新创建)
↓
线程数从 50 → 800(远超 maxPoolSize=200)
关键问题:线程数 800 > maxPoolSize 200,说明线程池配置有问题或线程不是从线程池创建的(比如直接 new Thread())。
方案 1:修复线程池配置(止血)
// 问题:线程池队列太大 + 最大线程数太小
new ThreadPoolExecutor(
50, // corePoolSize
200, // maxPoolSize(太小)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 队列太大(任务堆积)
new ThreadPoolExecutor.AbortPolicy()
);
// 修复:按 Little's Law 计算
// 线程数 = QPS × 平均耗时 = 500 × 0.05 = 25
// 但考虑到 GC 和锁竞争,需要 2 倍缓冲 = 50
new ThreadPoolExecutor(
50, // corePoolSize(按理论值)
100, // maxPoolSize(2 倍缓冲)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 队列缩小(只缓冲短暂波动)
new ThreadPoolExecutor.CallerRunsPolicy() // 反压到调用方
);
方案 2:修复 GC 问题(降压)
// 如果 Old 区满是因为内存泄漏:
// 问题:静态 Map 无限增长
static Map<String, Object> cache = new HashMap<>();
// 修复:用 Caffeine 带过期策略
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.build();
# 如果 Old 区满是因为对象过早晋升,JVM 参数调整:
-XX:G1HeapRegionSize=16m # 调大 Region 减少 Humongous
-XX:InitiatingHeapOccupancyPercent=35 # 提前启动并发标记
-XX:MaxGCPauseMillis=200 # 目标暂停时间
方案 3:修复锁竞争(根治)
// 问题:大锁 + 锁内 IO
synchronized (lock) {
// 慢操作:数据库查询、HTTP 调用
Result result = dbQuery();
// 慢操作:网络调用
ExternalApi.call(result);
// 更新共享状态
state.update(result);
}
// 修复 1:缩小锁粒度
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 只做必要操作
state.update(result);
} finally {
lock.unlock();
}
// IO 操作移到锁外
Result result = dbQuery();
ExternalApi.call(result);
// 修复 2:用并发集合替代锁
// 原:
Map<String, Object> state = new HashMap<>();
synchronized (state) {
state.put(key, value);
}
// 修复后:
ConcurrentHashMap<String, Object> state = new ConcurrentHashMap<>();
state.put(key, value); // 无锁
// 修复 3:用 ReadWriteLock(读多写少场景)
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
try {
Object value = state.get(key); // 读操作,多个线程可同时持有
} finally {
rwLock.readLock().unlock();
}
@Service
public class HealthMonitor {
// 1. CPU 监控
@Scheduled(fixedDelay = 60000)
public void checkCpu() {
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
double cpuLoad = osBean.getSystemCpuLoad();
if (cpuLoad > 0.8) {
alertService.warn("CPU 使用率过高: " + String.format("%.1f%%", cpuLoad * 100));
}
}
// 2. 线程数监控
@Scheduled(fixedDelay = 60000)
public void checkThreads() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long threadCount = threadBean.getThreadCount();
long peakCount = threadBean.getPeakThreadCount();
if (threadCount > 200) {
alertService.warn("线程数过高: " + threadCount + " (峰值: " + peakCount + ")");
}
// 检查 BLOCKED 线程
long blockedThreads = Arrays.stream(threadBean.getThreadInfo(
threadBean.getAllThreadIds()))
.filter(t -> t != null && t.getThreadState() == Thread.State.BLOCKED)
.count();
if (blockedThreads > 50) {
alertService.warn("BLOCKED 线程过多: " + blockedThreads);
}
}
// 3. GC 监控
@Scheduled(fixedDelay = 60000)
public void checkGC() {
List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
long count = gc.getCollectionCount();
long time = gc.getCollectionTime();
// Full GC 频率 > 1 次/小时
if (gc.getName().contains("Full GC") && count > 1) {
alertService.warn("Full GC 频率过高: " + count + " 次");
}
// STW 耗时 > 500ms
if (time > 500) {
alertService.warn("GC STW 耗时过长: " + time + "ms");
}
}
}
// 4. 内存监控
@Scheduled(fixedDelay = 60000)
public void checkMemory() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
double usedRatio = (double) heapUsage.getUsed() / heapUsage.getMax();
if (usedRatio > 0.8) {
alertService.warn("堆内存使用率过高: " + String.format("%.1f%%", usedRatio * 100));
}
}
}
| 指标 | 警告阈值 | 严重阈值 | 告警动作 |
|---|---|---|---|
| CPU 使用率 | > 80% | > 90% | 警告 → 自动扩容 |
| 线程数 | > 200 | > 500 | 警告 → 检查线程池 |
| BLOCKED 线程 | > 50 | > 100 | 警告 → 检查锁竞争 |
| Full GC 频率 | > 1 次/小时 | > 3 次/小时 | 警告 → 检查内存 |
| STW 耗时 | > 500ms | > 1s | 警告 → 调 GC 参数 |
| 堆内存使用率 | > 80% | > 90% | 警告 → 检查泄漏 |
# GitHub Actions / Jenkins
- name: Performance Regression Test
run: |
# 1. 压测 30 分钟
./run_load_test.sh --duration 30m --qps 500
# 2. 采集指标
./collect_metrics.sh --metrics cpu,threads,gc,memory
# 3. 阈值检查
python3 check_performance.py \
--cpu-threshold 0.8 \
--thread-threshold 200 \
--full-gc-threshold 1 \
--stw-threshold 500
# 4. 超标则阻断上线
if [ $? -ne 0 ]; then
echo "Performance regression detected!"
exit 1
fi
| 子问题 | 核心方案 |
|---|---|
| 1. CPU 飙高定位 | top → Arthas thread → Arthas profiler → heapdump → MAT |
| 2. 线程暴涨修复 | 修复线程池配置(队列缩小 + CallerRunsPolicy)+ 修复 GC(调参数/修泄漏)+ 修复锁竞争(缩小锁粒度/并发集合) |
| 3. 预警机制 | 多维度监控(CPU/线程/GC/内存)+ 告警阈值 + 自动化处置 + CI/CD 防回归 |
关键认知:
场景:某订单服务使用自定义线程池处理异步任务,高峰期任务积压严重,响应延迟飙升。线程池配置如下:
new ThreadPoolExecutor(
10, // corePoolSize
50, // maxPoolSize
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // queue
new ThreadPoolExecutor.AbortPolicy()
);
问题:
maxPoolSize=50 基本没被触发?分析线程池任务提交流程。首先 50 没有被触发,原因应该是积压的队列太长了。一家积压的队列是 1000,然后这些队列基本都超时了,超时直接就触发了任务的异常处理,直接就抛出异常了。线程池提交流程,首先核心线程执行,然后如果核心线程满了,开始放到任务队列,这时候就开始等待,等待前置执行,执行完之后放核心线程,再去执行新的任务,从队列里拿。这时候如果执行,等待队列满了之后,还有任务进入,才开始用最大线程,用非核心线程去创建执行新的任务。从没有被触发情况来看,应该是大量的任务堆积,堆积到队列中,然后还没到触发的时候,就已经抛出异常了;从已知信息可以看到,哪怕是 QPS 到 2000,排队也才 800 加,也就是说还没触发到 1000,并没有触发到进行扩容的阶段。这时候如果想要提升 CPU 利用率,需要降低一下任务队列,然后提升一下核心的队伍数。核心平时 500 的 QPS,平均耗时 100 毫秒,这样的话应该是放 50 个线程更合理一点,核心线程,然后最大线程放到 100,按成核心线程乘 2 来算,而不是当下的这种配置。如果下游真的挂了,全面超时线程池里边应该执行任务都是自动抛出异常,就是在执行任务的时候设置异常,而不是等拒绝策略。拒绝策略怎么定义才不丢请求?这个可以在拒绝策略里边去抛出计划任务、抛出消息,去消息再去做补偿。
优点:
不足:
// ThreadPoolExecutor.execute() 源码逻辑(简化)
public void execute(Runnable command) {
int wc = workerCountOf池);
if (wc < corePoolSize) {
// 1. 核心线程未满 → 创建核心线程
addWorker(command, true);
} else if (workQueue.offer(command)) {
// 2. 核心线程已满 → 尝试入队
// 如果入队成功,任务就在队列里等着,不会创建新线程
// 关键:队列没满就不会创建新线程!
} else {
// 3. 队列也满了 → 尝试创建非核心线程(直到 maxPoolSize)
if (!addWorker(command, false)) {
// 4. 线程数也达到 max → 触发拒绝策略
reject(command);
}
}
}
关键结论:LinkedBlockingQueue(1000) 容量太大,队列几乎永远不会满,maxPoolSize 形同虚设。
| 参数 | 值 | 含义 |
|---|---|---|
| corePoolSize | 10 | 常驻线程 |
| maximumPoolSize | 50 | 最大线程 |
| workQueue | LinkedBlockingQueue(1000) | 容量 1000 的队列 |
| 平时 QPS | 500 | |
| 峰值 QPS | 2000 | |
| 平均耗时 | 100ms | |
| 下游超时 | 3s | |
| 任务排队 | 800+ | 队列快满了 |
10 个核心线程,每个处理 100ms
理论吞吐 = 10 / 0.1 = 100 QPS
但实际 QPS 是 500,说明任务堆积在队列里
队列容量 1000,当前堆积 800
→ 队列没满 → 不创建新线程
→ maxPoolSize=50 永远不会触发
任务堆积过程:
T1: 500 QPS 进入,10 个核心线程处理
→ 理论只能处理 100 QPS
→ 400 QPS 任务进入队列
T2: 队列堆积到 800
→ 队列还有 200 空间
→ 不触发 maxPoolSize
T3: 下游超时 3s
→ 10 个核心线程全部阻塞
→ 新任务无法处理
→ 队列继续堆积
T4: 队列满到 1000
→ 开始创建非核心线程(直到 50)
→ 但此时任务已经超时了
| 问题 | 原因 | 影响 |
|---|---|---|
| 队列太大(1000) | 任务堆积 800,还没满 | maxPoolSize 永远不触发 |
| 核心线程太少(10) | 理论吞吐 100 QPS,实际需求 500 | 任务快速堆积 |
| 下游超时 3s | 线程阻塞在下游调用 | 线程池被拖死 |
| 没有超时保护 | 任务执行超时才抛异常 | 线程被占用 3 秒 |
线程数 = QPS × 平均耗时
= 500 × 0.1
= 50
理论需要 50 个线程才能消化 500 QPS。当前 core=10 只有理论值的 1/5,所以任务快速堆积。
new ThreadPoolExecutor(
50, // corePoolSize: 按理论值设置
100, // maxPoolSize: 2 倍缓冲
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 队列缩小!只缓冲短暂波动
new ThreadPoolExecutor.CallerRunsPolicy() // 反压到调用方
);
| 参数 | 原值 | 新值 | 推理 |
|---|---|---|---|
| corePoolSize | 10 | 50 | Little's Law:500 QPS × 100ms = 50 线程 |
| maximumPoolSize | 50 | 100 | 2 倍缓冲,应对下游抖动 |
| workQueue | 1000 | 100 | 关键!队列只能缓冲短暂波动 |
| keepAliveTime | 60s | 60s | 不变 |
| 拒绝策略 | AbortPolicy | CallerRunsPolicy | 不丢请求,反压到调用方 |
原方案: 队列1000 → 任务堆积到800 → 延迟 = 800/100 = 8秒
新方案: 队列100 → 堆积到100 → 延迟 = 100/500 = 0.2秒
队列的作用是缓冲短暂的流量波动(比如 1-2 秒的尖峰),不是用来"存任务"的。队列太大 = 延迟失控。
50 个核心线程全部阻塞在下游调用上(每个 3 秒)
→ 理论吞吐 = 50 / 3 = 16.7 QPS(从 500 降到 16.7)
→ 新任务无法处理
→ 队列堆积到 100
→ 超过 100 个线程也打满
→ 触发拒绝策略
→ 如果拒绝策略是 AbortPolicy,请求直接丢失
核心问题:线程池没有"自我保护"机制,下游挂了会拖死自己。
第一层:超时控制(最关键)
// 所有下游调用必须设超时
Future<Response> future = executor.submit(() -> downstream.call());
Response resp = future.get(500, TimeUnit.MILLISECONDS); // 强制 500ms 超时
或直接用 HTTP 客户端的超时配置:
HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1))
.responseTimeout(Duration.ofSeconds(2)) // 响应超时
.build();
关键:下游调用超时时间 < 线程池队列延迟容忍度
下游超时 500ms
队列 100 个任务
队列延迟 = 100 / 500 = 0.2s
总延迟 = 0.2s + 0.5s = 0.7s(可接受)
第二层:熔断器(Sentinel / Resilience4j)
@SentinelResource(
value = "orderProcess",
fallback = "orderProcessFallback",
circuitBreaker = @SentinelCircuitBreaker(
slowRatioThreshold = 0.5, // 慢调用比例 > 50% 熔断
slowRatioMaxRt = 500, // 慢调用阈值 500ms
minRequestAmount = 10, // 最少 10 个请求才触发
timeWindow = 10 // 熔断 10 秒
)
)
public void processOrder(OrderRequest req) {
downstream.call(req);
}
public void orderProcessFallback(OrderRequest req, Throwable ex) {
// 熔断后走补偿逻辑
log.warn("Order process fallback, sending to MQ for retry", ex);
mqProducer.send("order_retry", req);
}
第三层:自定义拒绝策略
方案 A:CallerRunsPolicy(反压到调用方)
new ThreadPoolExecutor.CallerRunsPolicy()
// 效果:调用方线程亲自执行任务,阻塞调用方,形成天然反压
// 适用:同步调用场景
方案 B:MQ 补偿(异步重试)
class MqCompensationHandler implements RejectedExecutionHandler {
@Autowired
private MqProducer mqProducer;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.warn("Task rejected, sending to MQ for retry: {}", r);
// 1. 发送到 MQ(保证不丢)
try {
mqProducer.send("order_retry", serialize(r));
} catch (Exception e) {
// 2. MQ 也失败了,记录到本地文件(最后兜底)
log.error("MQ send failed, writing to local file", e);
writeToFile(r);
}
}
private void writeToFile(Runnable r) {
// 写到本地文件,定时任务扫描重试
Files.write(Paths.get("/tmp/degraded-tasks/" + System.currentTimeMillis() + ".task"),
serialize(r).getBytes());
}
}
// MQ 消费者:异步重试
@RocketMQMessageListener(topic = "order_retry", consumerGroup = "retry_group")
public class OrderRetryConsumer implements RocketMQListener<Message> {
@Override
public void onMessage(Message message) {
OrderRequest req = JSON.parseObject(message.getBody(), OrderRequest.class);
// 重试逻辑
processOrder(req);
}
}
方案 C:业务降级 + 回调通知
class BusinessDegradeHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof OrderTask) {
// 订单场景:降级处理,返回默认结果
log.warn("Order task rejected, degrade to default");
OrderTask task = (OrderTask) r;
task.onRejected(); // 回调通知业务方
} else {
// 其他场景:直接拒绝
throw new RejectedExecutionException("Task rejected");
}
}
}
第四层:线程池隔离
// 下游 A 的线程池
ThreadPoolExecutor downstreamAExecutor = new ThreadPoolExecutor(
20, 50, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new CallerRunsPolicy()
);
// 下游 B 的线程池
ThreadPoolExecutor downstreamBExecutor = new ThreadPoolExecutor(
20, 50, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new CallerRunsPolicy()
);
// 调用时路由到对应线程池
ExecutorService executor = downstream.equals("A")
? downstreamAExecutor
: downstreamBExecutor;
隔离效果:
下游 A 挂了 → 只影响 A 的线程池(20-50 线程)
下游 B 正常 → B 的线程池不受影响(20-50 线程)
vs 共用线程池:
下游 A 挂了 → 所有线程被 A 占满 → B 也无法执行
| 子问题 | 核心方案 |
|---|---|
| maxPoolSize 没触发 | 队列容量太大(1000),改成 100 |
| 参数调优 | core=50, max=100, queue=100, CallerRunsPolicy |
| 下游挂掉 | 超时控制 + 熔断器 + MQ 补偿 + 线程池隔离 |
关键认知:
场景:某电商订单系统,需要支持"下单-扣库存-发消息"三件事最终一致,允许最多 30s 内不一致。
问题:
(未作答,直接讲解)
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 强一致 | 低 | 高 | 银行转账、跨 DB 事务 |
| TCC(Try-Confirm-Cancel) | 强一致 | 中 | 高 | 支付、库存扣减 |
| 本地消息表 + MQ | 最终一致 | 高 | 中 | 订单、通知、日志 |
| Saga | 最终一致 | 高 | 中 | 长流程事务 |
推荐方案:本地消息表 + 可靠消息(最终一致性)
理由:
1. 用户下单 → 订单服务
2. 订单服务开启本地事务:
a. 落订单(状态=待支付)到 MySQL
b. 落本地消息表(状态=待发送)
c. 提交本地事务
3. 定时任务扫描本地消息表 → 发送 MQ 消息
4. 库存服务消费 MQ:扣减库存(幂等)
5. 订单服务消费 MQ:更新订单状态(幂等)
6. 消息消费者确认消费成功 → 删除本地消息表记录
时序图:
用户 → 订单服务 → [MySQL 订单表 + 本地消息表](本地事务)
↓
定时任务扫描 → 发送 MQ
↓
库存服务 ← MQ 消息 → 订单服务
(扣库存) (更新状态)
↓
确认消费 → 删除消息表记录
-- 本地消息表
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL, -- 业务唯一 ID(订单号)
topic VARCHAR(64) NOT NULL, -- MQ topic
message_body TEXT NOT NULL, -- 消息内容
status TINYINT NOT NULL DEFAULT 0, -- 0=待发送, 1=已发送, 2=已消费
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
next_retry_time DATETIME, -- 下次重试时间
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status_retry (status, next_retry_time)
);
@Transactional
public void createOrder(OrderRequest req) {
// 1. 落订单
orderDao.insert(buildOrder(req, OrderStatus.PENDING_PAYMENT));
// 2. 落本地消息表(同一个事务!)
localMessageDao.insert(LocalMessage.builder()
.bizId(req.getOrderId())
.topic("order_created")
.messageBody(JSON.toJSONString(req))
.status(0)
.nextRetryTime(LocalDateTime.now())
.build());
// 事务提交后,由定时任务异步发送 MQ
}
// 定时任务:扫描待发送消息
@Scheduled(fixedDelay = 1000)
public void scanAndSendMessages() {
List<LocalMessage> messages = localMessageDao.selectPending(100);
for (LocalMessage msg : messages) {
try {
rocketMqProducer.send(msg.getTopic(), msg.getMessageBody());
localMessageDao.updateStatus(msg.getId(), 1); // 标记已发送
} catch (Exception e) {
log.error("Send message failed", e);
localMessageDao.incrementRetry(msg.getId()); // 增加重试次数
}
}
}
| 环节 | 幂等键 | 实现方式 |
|---|---|---|
| 订单创建 | order_id | 唯一索引,重复插入报错 |
| 库存扣减 | order_id + sku_id | 唯一索引 + 乐观锁 |
| 消息消费 | msg_id(MQ 消息 ID) | 去重表,消费前检查 |
// 库存服务消费 MQ 时的幂等处理
@Transactional
public void deductStock(StockMessage msg) {
// 1. 检查是否已处理
if (deductionRecordDao.exists(msg.getOrderId(), msg.getSkuId())) {
log.info("Already processed, skip: {}", msg.getOrderId());
return; // 幂等:重复消息直接跳过
}
// 2. 扣减库存(乐观锁)
int affected = stockDao.deductStockWithVersion(
msg.getSkuId(), msg.getQuantity(), msg.getVersion()
);
if (affected == 0) {
// 版本冲突,重试或失败
throw new OptimisticLockException("并发冲突");
}
// 3. 记录扣减记录(幂等依据)
deductionRecordDao.insert(DeductionRecord.builder()
.orderId(msg.getOrderId())
.skuId(msg.getSkuId())
.quantity(msg.getQuantity())
.build());
}
// 消息消费幂等(基于 MQ 消息 ID)
@RocketMQMessageListener(topic = "order_created", consumerGroup = "order_group")
public class OrderMessageConsumer implements RocketMQListener<Message> {
@Override
public void onMessage(Message message) {
String msgId = message.getMsgId();
// 1. 检查是否已消费
if (messageConsumeRecordDao.exists(msgId)) {
log.info("Message already consumed, skip: {}", msgId);
return;
}
// 2. 处理消息
OrderMessage msg = JSON.parseObject(message.getBody(), OrderMessage.class);
processOrder(msg);
// 3. 记录消费记录
messageConsumeRecordDao.insert(msgId);
}
}
| 根因 | 概率 | 说明 |
|---|---|---|
| 缓存与 DB 不一致 | 高 | 缓存有库存,DB 实际已卖完,读到脏数据 |
| 并发扣减无锁 | 中 | 两个请求同时读到库存=1,都扣成功 |
| 扣减非原子操作 | 中 | SELECT + UPDATE 非原子,中间有间隙 |
| 缓存过期时间不同步 | 低 | 缓存延迟导致短时间脏读 |
典型超卖场景:
T1: 请求 A 读缓存 → 库存=1
T2: 请求 B 读缓存 → 库存=1(缓存未更新)
T3: 请求 A 扣减 DB → 成功(库存=0)
T4: 请求 B 扣减 DB → 成功(库存=-1)← 超卖!
适用场景:高并发、库存精度要求不高(允许少量误差)
// 下单时:先扣 Redis 库存
public boolean preDeductStock(Long skuId, int quantity) {
String key = "stock:" + skuId;
// Lua 脚本保证原子性
String luaScript = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
""";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(quantity)
);
return result != null && result >= 0;
}
// 支付成功后:DB 最终确认
@Transactional
public void confirmStock(Long skuId, int quantity) {
int affected = stockDao.deductStockWithVersion(skuId, quantity);
if (affected == 0) {
// DB 扣减失败,回补 Redis
redisTemplate.opsForValue().increment("stock:" + skuId, quantity);
throw new InsufficientStockException("库存不足");
}
}
// 支付超时:回补 Redis
@Scheduled(fixedDelay = 60000)
public void rollbackTimeoutOrders() {
List<Order> timeoutOrders = orderDao.selectTimeout(30, TimeUnit.MINUTES);
for (Order order : timeoutOrders) {
redisTemplate.opsForValue().increment(
"stock:" + order.getSkuId(), order.getQuantity()
);
orderDao.updateStatus(order.getId(), OrderStatus.CANCELLED);
}
}
缓存与 DB 同步策略(Cache Aside):
@Transactional
public void deductStockWithVersion(Long skuId, int quantity) {
// 1. 更新 DB
int affected = stockDao.deductStockWithVersion(skuId, quantity);
// 2. 删除缓存(不是更新!)
redisTemplate.delete("stock:" + skuId);
return affected;
}
// 读缓存 miss 时回源
public long getStock(Long skuId) {
String key = "stock:" + skuId;
Long stock = redisTemplate.opsForValue().get(key);
if (stock == null) {
stock = stockDao.getStock(skuId);
// 随机过期时间(防雪崩)
long expire = 3600 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, stock, expire, TimeUnit.SECONDS);
}
return stock;
}
优点:
缺点:
适用场景:库存精度要求高、并发量中等(< 1 万 QPS)
// 库存表加 version 字段
ALTER TABLE stock ADD COLUMN version INT NOT NULL DEFAULT 0;
// 扣减时乐观锁
@Transactional
public void deductStockOptimistic(Long skuId, int quantity, int maxRetry) {
for (int i = 0; i < maxRetry; i++) {
Stock stock = stockDao.select(skuId);
if (stock.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}
int affected = stockDao.updateWithVersion(
skuId, quantity, stock.getVersion()
);
if (affected > 0) {
return; // 扣减成功
}
// 版本冲突,重试
}
throw new OptimisticLockException("并发冲突,重试失败");
}
优点:
缺点:
| 对比项 | 方案 A(Redis 预扣减) | 方案 B(DB 乐观锁) |
|---|---|---|
| 并发能力 | 10 万+ QPS | < 1 万 QPS |
| 一致性 | 最终一致(短暂窗口) | 强一致 |
| 超卖概率 | 极低(< 0.01%) | 无(乐观锁保证) |
| 实现复杂度 | 高(需处理缓存同步) | 低 |
| 适用场景 | 秒杀、大促 | 常规销售 |
@Scheduled(cron = "0 0 * * * ?")
public void reconcile() {
// 1. 扫描订单表 vs 库存表 vs 消息表
List<ReconciliationDiff> diffs = reconciliationDao.selectDiffs();
for (ReconciliationDiff diff : diffs) {
if (canAutoFix(diff)) {
autoFix(diff); // 自动修复
} else if (needAlert(diff)) {
sendAlert(diff); // 告警
} else {
createManualTicket(diff); // 人工介入
}
}
}
private boolean canAutoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
case MESSAGE_SENT_ORDER_NOT_UPDATED:
return true; // 可自动修复
default:
return false;
}
}
@Transactional
public void autoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
stockService.deductStock(diff.getSkuId(), diff.getQuantity());
break;
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
stockService.rollbackStock(diff.getSkuId(), diff.getQuantity());
break;
case MESSAGE_SENT_ORDER_NOT_UPDATED:
orderService.updateStatus(diff.getOrderId(), diff.getExpectedStatus());
break;
}
// 记录修复日志
reconciliationFixLogDao.insert(FixLog.builder()
.diffId(diff.getId())
.fixType(diff.getDiffType())
.fixTime(LocalDateTime.now())
.fixBy("system")
.build());
}
| 异常场景 | 影响 | 兜底方案 |
|---|---|---|
| 消息发送失败 | 订单已落,库存未扣 | 本地消息表重试 + 对账修复 |
| 消息消费失败 | 库存已扣,订单未更新 | 幂等重试 + 对账修复 |
| DB 写入失败 | 订单未落 | 事务回滚 + 本地消息表不写 |
| Redis 宕机 | 库存无法扣减 | 降级到 DB 直接扣减(限流保护) |
| MQ 宕机 | 消息无法发送 | 本地消息表堆积 + 延迟重试 |
| 库存超卖 | 卖多了 | 对账修复 + 人工介入 |
| 支付超时 | 库存未回补 | 定时任务扫描 + 自动回补 |
@SentinelResource(
value = "createOrder",
fallback = "createOrderFallback",
circuitBreaker = @SentinelCircuitBreaker(
slowRatioThreshold = 0.5,
slowRatioMaxRt = 500,
minRequestAmount = 10,
timeWindow = 10
)
)
public OrderResult createOrder(OrderRequest req) {
return orderService.createOrder(req);
}
public OrderResult createOrderFallback(OrderRequest req, Throwable ex) {
log.warn("Order creation fallback", ex);
// 降级:返回友好提示
return OrderResult.fail("系统繁忙,请稍后再试");
}
private void sendAlert(ReconciliationDiff diff) {
// 1. 钉钉/企微告警
AlertMsg msg = AlertMsg.builder()
.level("WARN")
.title("订单对账差异")
.content(String.format("订单 %s 库存差异 %d,需人工确认",
diff.getOrderId(), diff.getQuantity()))
.diffId(diff.getId())
.build();
alertService.send(msg);
}
private void createManualTicket(ReconciliationDiff diff) {
// 1. 创建工单
Ticket ticket = Ticket.builder()
.title("订单对账差异需人工处理")
.content(diff.getDetail())
.priority(diff.getQuantity() > 10 ? "HIGH" : "MEDIUM")
.assignee("ops_team")
.build();
ticketService.create(ticket);
// 2. 标记差异状态
diff.setStatus(DiffStatus.PENDING_MANUAL);
reconciliationDao.update(diff);
}
| 子问题 | 核心方案 |
|---|---|
| 1. 最终一致性 | 本地消息表 + 可靠 MQ + 幂等设计 |
| 2. 超卖 0.3% | 根因:缓存不一致 + 并发无锁;方案 A:Redis 预扣减(高并发);方案 B:DB 乐观锁(强一致) |
| 3. 异常兜底 | 对账修复 + 限流熔断 + 告警人工介入 |
关键设计原则:
| 轮次 | 方向 | 难度 | 表现 | 得分 |
|---|---|---|---|---|
| R1 | 性能优化 | 进阶 | 根因分析清晰,工具链方向正确 | 80/100 |
| R2 | 线程池优化 | 进阶 | 参数计算正确,但队列问题没讲透 | 75/100 |
| R3 | 架构设计 | 高阶 | 未作答,讲解完整 | - |
用户回答:
分析 GPU 飙高,首先看具体是哪个线程,可以通过 top 去查看具体的线程 ID,明确了 ID 之后可以看出来具体是哪个 ID,然后哪个 ID 导致信息又是过高。然后进一步想看内部细节的话,可以使用 Arris,然后登录它的看板,线程,Thread 去看一下具体是哪些线程导致的当前 CPU 过高占用,然后去明确线程的情况。 线程数上涨,从它的具体情况看,应该是大量的线程被占用了。然后把任务把线程栈打满,然后去扩展到非核心线程数,然后去进行使用,最终导致大量的线程创建。其实这个问题最终还是要确认为什么会有大量的线程超时,响应超时。这个超时的原因具体是因为什么?从当前得到的数据看,超时应该是因为 GC,大量的 GC,FullGC,5 分钟一次,其中堆内存有说 old 占的比例非常高的区,然后伊甸区频繁回收,也就是大量的 YoungGC 产生,这有可能是导致线程数暴涨的一个原因。在每个线程执行任务的时候,都导致了很多的对象创建,最终发生 GC,然后导致整个线程缓慢,然后再创建新的线程,线程伊甸一点点叠加出来,出现当下的情况。然后老年代没有被回收,确认一下是对象泄露了还是什么原因,具体对象是怎么回事?伊甸区频繁回收,也就是年轻代频繁回收的情况下,老年代还很满,也就是说有大量的对象存活了超长的时间。另外最后一点,线程有大量的线程卡在同步那,比如同步块中有东西缓慢,可能是数据库,可能是一些外部调用,可能是一些 IO 情况,最终导致线程的阻塞,导致了当前线程缓慢,其他线程阻塞,最终导致整个服务拖慢,然后创建大量线程去开新的任务,最终导致当下的问题。
点评:
讲解重点:
用户回答:
首先 50 没有被触发,原因应该是积压的队列太长了。一家积压的队列是 1000,然后这些队列基本都超时了,超时直接就触发了任务的异常处理,直接就抛出异常了。线程池提交流程,首先核心线程执行,然后如果核心线程满了,开始放到任务队列,这时候就开始等待,等待前置执行,执行完之后放核心线程,再去执行新的任务,从队列里拿。这时候如果执行,等待队列满了之后,还有任务进入,才开始用最大线程,用非核心线程去创建执行新的任务。从没有被触发情况来看,应该是大量的任务堆积,堆积到队列中,然后还没到触发的时候,就已经抛出异常了;从已知信息可以看到,哪怕是 QPS 到 2000,排队也才 800 加,也就是说还没触发到 1000,并没有触发到进行扩容的阶段。这时候如果想要提升 CPU 利用率,需要降低一下任务队列,然后提升一下核心的队伍数。核心平时 500 的 QPS,平均耗时 100 毫秒,这样的话应该是放 50 个线程更合理一点,核心线程,然后最大线程放到 100,按成核心线程乘 2 来算,而不是当下的这种配置。如果下游真的挂了,全面超时线程池里边应该执行任务都是自动抛出异常,就是在执行任务的时候设置异常,而不是等拒绝策略。拒绝策略怎么定义才不丢请求?这个可以在拒绝策略里边去抛出计划任务、抛出消息,去消息再去做补偿。
点评:
讲解重点:
用户回答:未作答,直接讲解
讲解重点:
文档版本:v1.0
最后更新:2026-09-14
总题数:3 题
总字数:约 10,000 字