← 每日面试题 学习看板

Java 后端模拟面试题 - 第二轮

面试题/高阶题目/2026-09-14-r2-性能-线程池-架构.md

Java 后端模拟面试题 - 第二轮

日期:2026-09-14

总题数:3 题(进阶 2 题 + 高阶 1 题)

方向覆盖:性能优化、线程池优化、架构设计


R1 性能优化 | 进阶

题目

场景:某 Java 服务 4 核 8G,上线 2 周后 CPU 从 30% 飙到 95%,线程数从 50 涨到 800,响应时间从 50ms 涨到 2s。监控显示:

问题:

  1. 分析 CPU 飙高的根因,给出定位步骤(工具链 + 关键指标)。
  2. 线程数从 50 涨到 800 的原因是什么?如何修复?
  3. 如何建立 CPU 飙高的预警机制,避免下次再发生?

原始回答

分析 GPU 飙高,首先看具体是哪个线程,可以通过 top 去查看具体的线程 ID,明确了 ID 之后可以看出来具体是哪个 ID,然后哪个 ID 导致信息又是过高。然后进一步想看内部细节的话,可以使用 Arris,然后登录它的看板,线程,Thread 去看一下具体是哪些线程导致的当前 CPU 过高占用,然后去明确线程的情况。 线程数上涨,从它的具体情况看,应该是大量的线程被占用了。然后把任务把线程栈打满,然后去扩展到非核心线程数,然后去进行使用,最终导致大量的线程创建。其实这个问题最终还是要确认为什么会有大量的线程超时,响应超时。这个超时的原因具体是因为什么?从当前得到的数据看,超时应该是因为 GC,大量的 GC,FullGC,5 分钟一次,其中堆内存有说 old 占的比例非常高的区,然后伊甸区频繁回收,也就是大量的 YoungGC 产生,这有可能是导致线程数暴涨的一个原因。在每个线程执行任务的时候,都导致了很多的对象创建,最终发生 GC,然后导致整个线程缓慢,然后再创建新的线程,线程伊甸一点点叠加出来,出现当下的情况。然后老年代没有被回收,确认一下是对象泄露了还是什么原因,具体对象是怎么回事?伊甸区频繁回收,也就是年轻代频繁回收的情况下,老年代还很满,也就是说有大量的对象存活了超长的时间。另外最后一点,线程有大量的线程卡在同步那,比如同步块中有东西缓慢,可能是数据库,可能是一些外部调用,可能是一些 IO 情况,最终导致线程的阻塞,导致了当前线程缓慢,其他线程阻塞,最终导致整个服务拖慢,然后创建大量线程去开新的任务,最终导致当下的问题。

点评

优点:

不足:


完整讲解

子问题 1:CPU 飙高定位步骤

1.1 因果链条
代码问题(内存泄漏 + 大对象分配 + 锁竞争)
    ↓
Young GC 频繁(大量短生命周期对象)
    ↓
Old 区快速填满(对象过早晋升 + 内存泄漏)
    ↓
Full GC 频繁(5 分钟一次,STW 1.5s)
    ↓
线程执行变慢(STW + 锁竞争)
    ↓
任务堆积 → 线程池创建新线程
    ↓
线程数从 50 暴涨到 800
    ↓
CPU 飙高(800 个线程上下文切换 + GC 线程)
    ↓
响应时间 50ms → 2s

关键洞察:这不是三个独立问题,而是一个根因引发的连锁反应。

1.2 工具链定位路径
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 分析 - 定位内存泄漏
1.3 关键命令
# 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
1.4 关键指标
指标正常值当前值诊断
CPU 使用率< 70%95%过高
线程数50-100800暴涨
Full GC 频率< 1 次/天3 次/小时过高
STW 耗时< 100ms1.5s过长
Old 区占用< 50%90%过高
Young GC 频率1-10 次/秒10+ 次/秒过高
BLOCKED 线程< 10150锁竞争严重

子问题 2:线程数暴涨原因 + 修复方案

2.1 线程暴涨原因
线程池配置: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())。

2.2 修复方案

方案 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();
}

子问题 3:预警机制设计

3.1 多维度监控指标
@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));
        }
    }
}
3.2 告警阈值配置
指标警告阈值严重阈值告警动作
CPU 使用率> 80%> 90%警告 → 自动扩容
线程数> 200> 500警告 → 检查线程池
BLOCKED 线程> 50> 100警告 → 检查锁竞争
Full GC 频率> 1 次/小时> 3 次/小时警告 → 检查内存
STW 耗时> 500ms> 1s警告 → 调 GC 参数
堆内存使用率> 80%> 90%警告 → 检查泄漏
3.3 CI/CD 防回归
# 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 防回归

关键认知:

  1. 三个问题是同一个根因:代码问题 → GC 频繁 → 线程变慢 → 线程暴涨 → CPU 飙高
  2. 修复要按优先级:先止血(线程池配置)→ 再降压(GC 调参)→ 后根治(修锁竞争)
  3. 预警是长效机制:监控 + 告警 + 自动化处置 + CI/CD 防回归
  4. 不要只看 CPU:CPU 飙高是结果,根因可能是 GC、内存泄漏、锁竞争

R2 线程池优化 | 进阶

题目

场景:某订单服务使用自定义线程池处理异步任务,高峰期任务积压严重,响应延迟飙升。线程池配置如下:

new ThreadPoolExecutor(
    10,                          // corePoolSize
    50,                          // maxPoolSize
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),  // queue
    new ThreadPoolExecutor.AbortPolicy()
);

问题:

  1. 为什么 maxPoolSize=50 基本没被触发?分析线程池任务提交流程。
  2. 如何调整参数让高峰期平滑处理?给出具体数值和推理。
  3. 如果下游真的挂了(全超时),线程池应该怎么设计?拒绝策略怎么自定义才不丢请求?

原始回答

首先 50 没有被触发,原因应该是积压的队列太长了。一家积压的队列是 1000,然后这些队列基本都超时了,超时直接就触发了任务的异常处理,直接就抛出异常了。线程池提交流程,首先核心线程执行,然后如果核心线程满了,开始放到任务队列,这时候就开始等待,等待前置执行,执行完之后放核心线程,再去执行新的任务,从队列里拿。这时候如果执行,等待队列满了之后,还有任务进入,才开始用最大线程,用非核心线程去创建执行新的任务。从没有被触发情况来看,应该是大量的任务堆积,堆积到队列中,然后还没到触发的时候,就已经抛出异常了;从已知信息可以看到,哪怕是 QPS 到 2000,排队也才 800 加,也就是说还没触发到 1000,并没有触发到进行扩容的阶段。这时候如果想要提升 CPU 利用率,需要降低一下任务队列,然后提升一下核心的队伍数。核心平时 500 的 QPS,平均耗时 100 毫秒,这样的话应该是放 50 个线程更合理一点,核心线程,然后最大线程放到 100,按成核心线程乘 2 来算,而不是当下的这种配置。如果下游真的挂了,全面超时线程池里边应该执行任务都是自动抛出异常,就是在执行任务的时候设置异常,而不是等拒绝策略。拒绝策略怎么定义才不丢请求?这个可以在拒绝策略里边去抛出计划任务、抛出消息,去消息再去做补偿。

点评

优点:

不足:


完整讲解

子问题 1:为什么 maxPoolSize 没被触发

1.1 线程池任务提交流程
// 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 形同虚设。

1.2 本场景数据
参数值含义
corePoolSize10常驻线程
maximumPoolSize50最大线程
workQueueLinkedBlockingQueue(1000)容量 1000 的队列
平时 QPS500
峰值 QPS2000
平均耗时100ms
下游超时3s
任务排队800+队列快满了
1.3 为什么 maxPoolSize=50 没被触发
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)
    → 但此时任务已经超时了
1.4 问题本质
问题原因影响
队列太大(1000)任务堆积 800,还没满maxPoolSize 永远不触发
核心线程太少(10)理论吞吐 100 QPS,实际需求 500任务快速堆积
下游超时 3s线程阻塞在下游调用线程池被拖死
没有超时保护任务执行超时才抛异常线程被占用 3 秒

子问题 2:参数调优方案

2.1 核心公式:Little's Law
线程数 = QPS × 平均耗时
     = 500 × 0.1
     = 50

理论需要 50 个线程才能消化 500 QPS。当前 core=10 只有理论值的 1/5,所以任务快速堆积。

2.2 推荐配置
new ThreadPoolExecutor(
    50,                          // corePoolSize: 按理论值设置
    100,                         // maxPoolSize: 2 倍缓冲
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),  // 队列缩小!只缓冲短暂波动
    new ThreadPoolExecutor.CallerRunsPolicy()  // 反压到调用方
);
2.3 参数推理
参数原值新值推理
corePoolSize1050Little's Law:500 QPS × 100ms = 50 线程
maximumPoolSize501002 倍缓冲,应对下游抖动
workQueue1000100关键!队列只能缓冲短暂波动
keepAliveTime60s60s不变
拒绝策略AbortPolicyCallerRunsPolicy不丢请求,反压到调用方
2.4 为什么队列要缩到 100
原方案: 队列1000 → 任务堆积到800 → 延迟 = 800/100 = 8秒
新方案: 队列100 → 堆积到100 → 延迟 = 100/500 = 0.2秒

队列的作用是缓冲短暂的流量波动(比如 1-2 秒的尖峰),不是用来"存任务"的。队列太大 = 延迟失控。

2.5 为什么 maxPoolSize 不用 200

子问题 3:下游挂掉时的设计

3.1 场景分析
50 个核心线程全部阻塞在下游调用上(每个 3 秒)
→ 理论吞吐 = 50 / 3 = 16.7 QPS(从 500 降到 16.7)
→ 新任务无法处理
→ 队列堆积到 100
→ 超过 100 个线程也打满
→ 触发拒绝策略
→ 如果拒绝策略是 AbortPolicy,请求直接丢失

核心问题:线程池没有"自我保护"机制,下游挂了会拖死自己。

3.2 方案:多层防护

第一层:超时控制(最关键)

// 所有下游调用必须设超时
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 补偿 + 线程池隔离

关键认知:

  1. 队列不是越大越好,大队列 = 高延迟 + maxPoolSize 失效
  2. 拒绝策略不是兜底,是反压机制的一部分
  3. 超时 + 熔断 + 隔离是线程池自我保护的三件套
  4. 监控 + 动态调整是运维的核心手段

R3 架构设计 | 高阶

题目

场景:某电商订单系统,需要支持"下单-扣库存-发消息"三件事最终一致,允许最多 30s 内不一致。

问题:

  1. 设计一个最终一致性方案,覆盖:扣库存、落订单、发消息,画出调用时序和幂等点。
  2. 库存超卖 0.3% 的根因可能是什么?给出 2 种解决方案及适用场景。
  3. 如果对账发现差异(订单状态不一致),如何设计自动修复 + 告警 + 人工介入的闭环?

原始回答

(未作答,直接讲解)

完整讲解

子问题 1:最终一致性方案设计

1.1 方案选型
方案一致性性能复杂度适用场景
2PC(两阶段提交)强一致低高银行转账、跨 DB 事务
TCC(Try-Confirm-Cancel)强一致中高支付、库存扣减
本地消息表 + MQ最终一致高中订单、通知、日志
Saga最终一致高中长流程事务

推荐方案:本地消息表 + 可靠消息(最终一致性)

理由:

1.2 核心流程
1. 用户下单 → 订单服务
2. 订单服务开启本地事务:
   a. 落订单(状态=待支付)到 MySQL
   b. 落本地消息表(状态=待发送)
   c. 提交本地事务
3. 定时任务扫描本地消息表 → 发送 MQ 消息
4. 库存服务消费 MQ:扣减库存(幂等)
5. 订单服务消费 MQ:更新订单状态(幂等)
6. 消息消费者确认消费成功 → 删除本地消息表记录

时序图:

用户 → 订单服务 → [MySQL 订单表 + 本地消息表](本地事务)
                    ↓
              定时任务扫描 → 发送 MQ
                    ↓
         库存服务 ← MQ 消息 → 订单服务
         (扣库存)         (更新状态)
                    ↓
              确认消费 → 删除消息表记录
1.3 本地消息表实现
-- 本地消息表
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());   // 增加重试次数
        }
    }
}
1.4 幂等设计
环节幂等键实现方式
订单创建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);
    }
}

子问题 2:超卖 0.3% 根因与解法

2.1 根因分析
根因概率说明
缓存与 DB 不一致高缓存有库存,DB 实际已卖完,读到脏数据
并发扣减无锁中两个请求同时读到库存=1,都扣成功
扣减非原子操作中SELECT + UPDATE 非原子,中间有间隙
缓存过期时间不同步低缓存延迟导致短时间脏读

典型超卖场景:

T1: 请求 A 读缓存 → 库存=1
T2: 请求 B 读缓存 → 库存=1(缓存未更新)
T3: 请求 A 扣减 DB → 成功(库存=0)
T4: 请求 B 扣减 DB → 成功(库存=-1)← 超卖!
2.2 解决方案 A:Redis 预扣减 + DB 确认

适用场景:高并发、库存精度要求不高(允许少量误差)

// 下单时:先扣 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;
}

优点:

缺点:

2.3 解决方案 B:DB 乐观锁扣减

适用场景:库存精度要求高、并发量中等(< 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("并发冲突,重试失败");
}

优点:

缺点:

2.4 方案对比
对比项方案 A(Redis 预扣减)方案 B(DB 乐观锁)
并发能力10 万+ QPS< 1 万 QPS
一致性最终一致(短暂窗口)强一致
超卖概率极低(< 0.01%)无(乐观锁保证)
实现复杂度高(需处理缓存同步)低
适用场景秒杀、大促常规销售

子问题 3:异常场景兜底方案

3.1 对账 + 自动修复
@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());
}
3.2 异常场景清单
异常场景影响兜底方案
消息发送失败订单已落,库存未扣本地消息表重试 + 对账修复
消息消费失败库存已扣,订单未更新幂等重试 + 对账修复
DB 写入失败订单未落事务回滚 + 本地消息表不写
Redis 宕机库存无法扣减降级到 DB 直接扣减(限流保护)
MQ 宕机消息无法发送本地消息表堆积 + 延迟重试
库存超卖卖多了对账修复 + 人工介入
支付超时库存未回补定时任务扫描 + 自动回补
3.3 限流 + 熔断
@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("系统繁忙,请稍后再试");
}
3.4 告警 + 人工介入
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. 异常兜底对账修复 + 限流熔断 + 告警人工介入

关键设计原则:

  1. 本地事务保证原子性:订单 + 本地消息表必须在同一个事务里
  2. 幂等是基础:所有消费端、扣减端必须幂等
  3. 对账是兜底:即使方案有漏洞,对账系统能发现并修复
  4. 降级是保护:Redis/MQ 宕机时有降级方案
  5. 告警是通知:无法自动修复的差异需要人工介入

面试总结

各轮次表现汇总

轮次方向难度表现得分
R1性能优化进阶根因分析清晰,工具链方向正确80/100
R2线程池优化进阶参数计算正确,但队列问题没讲透75/100
R3架构设计高阶未作答,讲解完整-

强项

  1. 根因分析能力:能识别 GC 频繁、线程暴涨、CPU 飙高是同一个根因引发的连锁反应
  2. 工具链使用:知道用 top、Arthas、MAT 等工具定位问题
  3. 线程池原理:理解任务提交流程,能用 Little's Law 计算参数
  4. 架构思维:知道用 MQ 做补偿,方向正确

待提升点

  1. 预警机制设计:需要建立监控 + 告警 + 自动化处置的完整体系
  2. 队列问题理解:需要深入理解"队列太大导致 maxPoolSize 失效"的原理
  3. 超时保护设计:需要掌握超时控制、熔断器、线程池隔离的完整方案
  4. 方案完整性:回答问题时,确保覆盖所有子问题,特别是兜底方案
  5. 一致性设计:需要说清楚缓存与 DB 的一致性保证(对账、回补、降级)

建议

  1. 性能优化:深入学习 JVM GC 原理、Arthas 工具链、性能监控体系
  2. 线程池优化:掌握线程池参数调优、拒绝策略、超时控制、熔断隔离
  3. 架构设计:多练分布式事务、最终一致性、对账修复等经典场景
  4. 方案完整性:回答问题时,确保覆盖所有子问题,特别是兜底方案
  5. 预警机制:建立监控 + 告警 + 自动化处置的完整体系

会话全文记录

时间:2026-09-14(第二轮)

R1 性能优化

用户回答:

分析 GPU 飙高,首先看具体是哪个线程,可以通过 top 去查看具体的线程 ID,明确了 ID 之后可以看出来具体是哪个 ID,然后哪个 ID 导致信息又是过高。然后进一步想看内部细节的话,可以使用 Arris,然后登录它的看板,线程,Thread 去看一下具体是哪些线程导致的当前 CPU 过高占用,然后去明确线程的情况。 线程数上涨,从它的具体情况看,应该是大量的线程被占用了。然后把任务把线程栈打满,然后去扩展到非核心线程数,然后去进行使用,最终导致大量的线程创建。其实这个问题最终还是要确认为什么会有大量的线程超时,响应超时。这个超时的原因具体是因为什么?从当前得到的数据看,超时应该是因为 GC,大量的 GC,FullGC,5 分钟一次,其中堆内存有说 old 占的比例非常高的区,然后伊甸区频繁回收,也就是大量的 YoungGC 产生,这有可能是导致线程数暴涨的一个原因。在每个线程执行任务的时候,都导致了很多的对象创建,最终发生 GC,然后导致整个线程缓慢,然后再创建新的线程,线程伊甸一点点叠加出来,出现当下的情况。然后老年代没有被回收,确认一下是对象泄露了还是什么原因,具体对象是怎么回事?伊甸区频繁回收,也就是年轻代频繁回收的情况下,老年代还很满,也就是说有大量的对象存活了超长的时间。另外最后一点,线程有大量的线程卡在同步那,比如同步块中有东西缓慢,可能是数据库,可能是一些外部调用,可能是一些 IO 情况,最终导致线程的阻塞,导致了当前线程缓慢,其他线程阻塞,最终导致整个服务拖慢,然后创建大量线程去开新的任务,最终导致当下的问题。

点评:

讲解重点:


R2 线程池优化

用户回答:

首先 50 没有被触发,原因应该是积压的队列太长了。一家积压的队列是 1000,然后这些队列基本都超时了,超时直接就触发了任务的异常处理,直接就抛出异常了。线程池提交流程,首先核心线程执行,然后如果核心线程满了,开始放到任务队列,这时候就开始等待,等待前置执行,执行完之后放核心线程,再去执行新的任务,从队列里拿。这时候如果执行,等待队列满了之后,还有任务进入,才开始用最大线程,用非核心线程去创建执行新的任务。从没有被触发情况来看,应该是大量的任务堆积,堆积到队列中,然后还没到触发的时候,就已经抛出异常了;从已知信息可以看到,哪怕是 QPS 到 2000,排队也才 800 加,也就是说还没触发到 1000,并没有触发到进行扩容的阶段。这时候如果想要提升 CPU 利用率,需要降低一下任务队列,然后提升一下核心的队伍数。核心平时 500 的 QPS,平均耗时 100 毫秒,这样的话应该是放 50 个线程更合理一点,核心线程,然后最大线程放到 100,按成核心线程乘 2 来算,而不是当下的这种配置。如果下游真的挂了,全面超时线程池里边应该执行任务都是自动抛出异常,就是在执行任务的时候设置异常,而不是等拒绝策略。拒绝策略怎么定义才不丢请求?这个可以在拒绝策略里边去抛出计划任务、抛出消息,去消息再去做补偿。

点评:

讲解重点:


R3 架构设计

用户回答:未作答,直接讲解

讲解重点:


文档版本:v1.0

最后更新:2026-09-14

总题数:3 题

总字数:约 10,000 字