SpringBoot开发实战:从入门到精通

31 阅读1分钟

不知道你有没有遇到过这样的情况:一个运行稳定的SpringBoot服务,在流量突增时突然响应变慢,甚至出现线程堆积。当你翻遍代码没找到明显问题时,可能忽略了一个隐藏的坑——默认线程池的饥饿问题。 去年我们一个订单处理服务就栽在这上面。日常QPS 200时一切正常,但在大促期间QPS冲到1500后,系统响应时间从50ms飙升到2秒以上,线程数直接打满200(默认最大值)。下面是我从这次事故中总结的实战经验。

现象:线程池为什么突然不够用了?

表面看是流量增长导致资源不足,但深入分析后发现:默认的Tomcat线程池和异步任务共享同一个线程池。当你的Controller里有这样的代码:

@RestController
public class OrderController {
    @GetMapping("/create")
    public CompletableFuture<String> createOrder() {
        return CompletableFuture.supplyAsync(() -> {
            // 耗时操作
            heavyProcess(); 
            return "success";
        });
    }
}

你以为的异步非阻塞,实际上所有请求和异步任务都在抢同一个线程池!这就是SpringBoot的隐藏设定:@Async和CompletableFuture.supplyAsync默认共用Tomcat的工作线程池。

根因:线程池的两种抢占模式

通过jstack抓取线程堆栈后,我们发现两种典型阻塞场景:

  1. HTTP线程被异步任务占用
    当大量请求进入时,Tomcat线程不仅要处理HTTP请求,还要执行supplyAsync里的逻辑,相当于一个线程干两份活。

  2. 异步任务等待HTTP线程释放
    更糟的是,如果异步任务里又调用了其他HTTP接口(比如调用支付服务),会出现"线程A等线程B,线程B等线程A"的死锁式等待。

实测数据:当并发请求达到300时,使用默认配置的系统平均响应时间为1200ms,而隔离线程池后降到180ms。

解决方案:线程池隔离的正确姿势

错误示范:盲目增加线程数

server:
  tomcat:
    max-threads: 500 # 简单调大只会延迟崩溃时间

正确做法:分离HTTP和业务线程池

@Configuration
public class ThreadPoolConfig {
    
    // 专用于异步任务
    @Bean("businessTaskExecutor")
    public Executor businessExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("biz-task-");
        executor.initialize();
        return executor;
    }
}

// 使用时明确指定线程池
@RestController
public class OrderController {
    @GetMapping("/create")
    public CompletableFuture<String> createOrder() {
        return CompletableFuture.supplyAsync(() -> {
            heavyProcess();
            return "success";
        }, businessTaskExecutor); // 关键点:显式指定executor
    }
}

高级技巧:监控线程池状态

// 在ThreadPoolTaskExecutor配置后追加
executor.setRejectedExecutionHandler((r, executor) -> {
    log.warn("线程池满载! Active:{}, Queue:{}", 
        executor.getActiveCount(),
        executor.getQueue().size());
    throw new ServiceBusyException();
});

避坑清单:线程池使用的5个关键点

  1. 永远不要使用无界队列
    实测显示:队列长度超过1000时,99线会从50ms恶化到800ms

  2. 区分CPU密集和IO密集任务
    IO密集型(如网络调用)建议线程数 = CPU核数 * (1 + 平均等待时间/平均计算时间)

  3. 警惕@Async的默认行为
    记得加上@Async("指定的线程池"),否则会用SimpleAsyncTaskExecutor(每次新建线程)

  4. 线程池隔离要彻底
    HTTP/RPC/定时任务/消息消费建议分别用独立线程池

  5. 别忘了线程泄漏检测
    用ThreadPoolExecutor.setRejectedExecutionHandler捕获异常

结语:线程池是性能的放大器也是雷区

经过这次教训,我们现在对所有SpringBoot项目都会在启动时打印线程池配置摘要。记住:默认配置在高并发下就是最大的风险点,显式配置和隔离才是王道。

你们团队是怎么管理线程池的?有没有遇到过更诡异的线程问题?欢迎在评论区分享你的实战案例。