Spring Boot异步接口性能优化:从单线程到高并发的优化历程

Spring Boot异步接口性能优化:从单线程到高并发的优化历程

在现代Web应用开发中,接口性能往往是系统瓶颈的关键所在。特别是当业务逻辑涉及多个外部服务调用时,传统的同步处理方式会导致线程阻塞,严重影响系统吞吐量,因此进行优化。

业务场景描述

用户信息聚合接口需要从三个不同的数据源获取信息:

  • 基础信息查询:耗时100ms(数据库查询)
  • 详情信息查询:耗时300ms(复杂业务查询)
  • 扩展信息查询:耗时500ms(第三方API调用)

这是一个典型的IO密集型场景,同步执行总耗时900ms,而理论上并行执行只需要600ms(基础信息查询必须最先执行后面并行)。

同步版本

java 复制代码
@GetMapping("/user/sync")
public User getUserSync() throws InterruptedException {
    return userService.assembleUser();
}

public User assembleUser() throws InterruptedException {
    long startTime = System.currentTimeMillis();
    // 1. 获取基础信息(100ms)
    Long userId = getUserBaseInfo();

    // 2. 获取详细信息(300ms)
    UserDetail userDetail = getUserDetailInfo(userId);

    // 3. 获取扩展信息(500ms)
    UserExtraInfo extraInfo = getUserExtraInfo(userId);

    // 组装完整User对象
    User user = new User();
    user.setId(userId);
    user.setName(userDetail.getName());
    user.setAge(userDetail.getAge());
    user.setSex(userDetail.getSex());
    user.setPhone(extraInfo.getPhone());
    user.setLevel(extraInfo.getLevel());
    System.out.println("用户对象组装完成,总耗时: " + (System.currentTimeMillis() - startTime) + "ms");
    return user;
}

测试结果

  • 单次调用耗时:~900ms
  • 同步结果: User(id=1001, name=张三, age=25, sex=1, level=VIP会员, phone=13800138000)

异步并行

意识到这三个查询之间存在依赖关系(详情和扩展信息都需要userId),我设计了一个并行执行方案:

java 复制代码
@GetMapping("/user/async-ultra")  
public User getUserAsyncUltra() throws Exception {
    return userService.assembleUserUltraPerformance();
}

public User assembleUserUltraPerformance() throws Exception {
    long startTime = System.currentTimeMillis();
    
    // 先获取基础信息
    CompletableFuture<Long> baseInfoFuture = CompletableFuture.supplyAsync(() -> {
        try {
            return getUserBaseInfo();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }, highPerformanceExecutor);

    // 基于baseInfo并行获取详情和扩展信息
    CompletableFuture<UserDetail> detailFuture = baseInfoFuture.thenApplyAsync(userId -> {
        try {
            return getUserDetailInfo(userId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }, highPerformanceExecutor);

    CompletableFuture<UserExtraInfo> extraFuture = baseInfoFuture.thenApplyAsync(userId -> {
        try {
            return getUserExtraInfo(userId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }, highPerformanceExecutor);

    // 等待所有任务完成
    CompletableFuture<Void> allTasks = CompletableFuture.allOf(baseInfoFuture, detailFuture, extraFuture);
    allTasks.get();
    
   
    // 获取结果并组装(此时所有任务都已完成,不会阻塞)
    Long userId = baseInfoFuture.get();
    UserDetail detail = detailFuture.get();
    UserExtraInfo extra = extraFuture.get();

    User user = new User();
    user.setId(userId);
    user.setName(detail.getName());
    user.setAge(detail.getAge());
    user.setSex(detail.getSex());
    user.setPhone(extra.getPhone());
    user.setLevel(extra.getLevel());

    System.out.println("极致性能组装完成,总耗时: " + (System.currentTimeMillis() - startTime) + "ms");
    return user;   
}

测试结果

  • 单次调用耗时:~600ms(相比同步版本提升33%)
  • 异步结果: User(id=1001, name=张三, age=25, sex=1, level=VIP会员, phone=13800138000)

对比结果如下图:

问题发现:高并发下的性能反转

虽然单次调用异步版本更快,但在JMeter压测中却发现了一个令人困惑的现象:在高并发场景下,异步接口的吞吐量竟然没有明显提升!

压测环境

  • 硬件配置:8核CPU,16GB内存
  • JMeter配置 :150并发线程,60秒Ramp-up,180秒持续时间

压测结果如下图:

同步压测结果如图:

异步压测结果如图:

而且最严重的是:异步接口耗时时间显著增加
开始日志打印: 极致性能组装完成,总耗时: 601ms-711ms
后期日志打印: 极致性能组装完成,总耗时: 22441ms-22632ms

个人分析:

  • 150并发请求同时到达
  • 每个请求需要3个线程(baseInfo + detail + extra)
  • 理论需要: 150 × 3 =450个线程 实际只有: 最大12个线程
  • 线程严重不足!

线程池

初始的线程池配置:

java 复制代码
    @Bean("highPerformanceExecutor")
    public ThreadPoolTaskExecutor highPerformanceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 更保守的配置,减少线程切换
        executor.setCorePoolSize(6);  // 略小于CPU核数
        executor.setMaxPoolSize(12);  // CPU核数 * 1.5
        executor.setQueueCapacity(500);
        executor.setKeepAliveSeconds(30);
        executor.setThreadNamePrefix("HighPerf-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }

在150并发下,这个配置导致了严重的线程竞争。

线程池参数调优

java 复制代码
 @Bean("highPerformanceExecutor")
    public ThreadPoolTaskExecutor highPerformanceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 针对高并发调整
        executor.setCorePoolSize(50);   // 增加核心线程
        executor.setMaxPoolSize(150);   // 增加最大线程
        executor.setQueueCapacity(50);  // 减小队列,快速扩展线程
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("HighPerf-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }

压测结果如下:可以看到吞吐量得到了明显提升


数据对比:

同步接口: 137 TPS

异步接口(优化前): 6.6 TPS

异步接口(优化后): 142 TPS

关键发现:

异步接口TPS提升了 2050% (142/6.6)

相比同步接口提升了 3.6% (142/137)

继续增大线程数压测如下图: 吞吐量提升到了175

java 复制代码
@Bean("highPerformanceExecutor")
    public ThreadPoolTaskExecutor highPerformanceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 针对高并发调整
        executor.setCorePoolSize(100);   // 增加核心线程
        executor.setMaxPoolSize(300);   // 增加最大线程
        executor.setQueueCapacity(50);  // 减小队列,快速扩展线程
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("HighPerf-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }

分析:

不是设置得越高越好!虽然提升了吞吐量,但隐藏着很大的风险。这是一个典型的"用资源换性能"的策略,需要谨慎使用,而且后期的接口响应时间明显增加

1.🎯

每个线程栈大小:1MB (默认)

300个线程:300MB 栈内存

加上堆内存、方法区等:总内存需求激增

  • OutOfMemoryError
  • 频繁Full GC导致STW(Stop The World)
  • 系统响应变慢

2.📊

理想情况:8个线程同时运行

300个线程:需要频繁的上下文切换

切换成本:每次切换消耗几微秒到几毫秒

  • 轻微的压力就会创建大量线程,但线程数远远超过CPU处理能力
  • CPU利用率虚高但实际处理能力下降
  • 系统整体性能下降

3⚡

高并发(500+)下:严重不足

实际需求:500 × 3 = 1500个线程

结果:大量任务被CallerRunsPolicy拒绝,回到主线程执行

使用JVisualVM分析

  1. 线程数量激增:异步版本创建了大量线程
  2. CPU上下文切换频繁:8核CPU的上下文切换成为瓶颈
  3. 内存使用增加:每个线程占用约1MB栈空间

同步分析:

异步分析:

jvm参数增加堆的内存大小:-Xmx2g -Xms2g ,继续测试如下:


再次压测异步:

吞吐量前后差别不大,通过内存分析工具分析,之前的内存已经够用,GC发生次数也不多,内存增加反而可能带来负面影响:

  • GC暂停时间变长:虽然频率降低,但每次Full GC时间可能从100ms→2s
  • 内存扫描成本:G1GC需要扫描整个堆,堆越大扫描时间越长

线程切换次数优化

优化代码:减少一次线程切换

java 复制代码
public User assembleUserOptimized() throws Exception {
        long startTime = System.currentTimeMillis();

        // 在当前线程执行最快的任务
        Long userId = getUserBaseInfo(); // 100ms,无需异步

        // 只对耗时任务异步
        CompletableFuture<UserDetail> detailFuture = CompletableFuture.supplyAsync(() -> {
            try {
                return getUserDetailInfo(userId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }, highPerformanceExecutor);

        CompletableFuture<UserExtraInfo> extraFuture = CompletableFuture.supplyAsync(() -> {
            try {
                return getUserExtraInfo(userId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }, highPerformanceExecutor);

        // 并行等待
        UserDetail detail = detailFuture.get();
        UserExtraInfo extra = extraFuture.get();
        User user = new User();
        user.setId(userId);
        user.setName(detail.getName());
        user.setAge(detail.getAge());
        user.setSex(detail.getSex());
        user.setPhone(extra.getPhone());
        user.setLevel(extra.getLevel());

        System.out.println("高性能并行组装完成,总耗时: " + (System.currentTimeMillis() - startTime) + "ms");
        return user;
    
    }

压测结果如下:确实明显提升了吞吐量

优化Tomcat配置(提升点)

yaml 复制代码
server:
  tomcat:
    threads:
      max: 300        # 增加最大线程数
      min-spare: 50   # 增加最小线程数
    accept-count: 200 # 增加连接队列
    max-connections: 2000 # 增加最大连接数

动态线程池补充

在Java开发中,ThreadPoolExecutor的使用常常面临以下挑战:

  1. 参数配置难:核心参数凭经验设置,无法精准匹配业务场景
  2. 调整成本高:参数优化需修改代码重启服务,影响业务连续性
  3. 运行无感知:线程池负载状态缺乏监控,问题爆发后才能察觉

动态线程池参考:springboot整合动态线程池

虚拟线程

虚拟线程是 Java 中的一种轻量级线程,它旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,允许创建数千甚至数万个虚拟线程,而无需占用大量操作系统资源。

虚拟线程与传统线程具有如下几个优势:

  1. 更加轻量级:虚拟线程相比传统线程更加轻量级。因为它们不是直接映射到操作系统线程上,而是在用户空间内被管理。这种设计减少了线程创建和销毁的开销,允许在同一应用中运行成千上万的线程。
  2. 资源消耗更少:由于不是直接映射到操作系统线程,虚拟线程显著降低了内存和其他资源的消耗。这使得在有限资源下可以创建更多的线程。
  3. 上下文切换开销更低:由于虚拟线程在用户空间,而不是通过操作系统,所以它的上下文切换开销更低。
  4. 改善阻塞操作的处理:在传统线程模型中,阻塞操作(如 I/O)会导致整个线程被阻塞,浪费宝贵的系统资源。然而当一个虚拟线程阻塞时,它可以被挂起,底层的操作系统线程则可以用来运行其他虚拟线程。
  5. 简化并发编程:可以像编写普通顺序代码一样编写并发代码,而不需要过多考虑线程管理和调度。它简化了 Java 的并发编程模型。
  6. 提升性能:在 I/O 密集型应用中,虚拟线程能够显著地提升性能。而且由于它们的创建和销毁成本低,能够更加高效地利用系统资源。

参考学习: 虚拟线程

更改代码如下:

java 复制代码
 public User assembleUserVirtualThread() throws Exception {
        long startTime = System.currentTimeMillis();

        // 在当前线程执行最快的任务
        Long userId = getUserBaseInfo(); // 100ms,无需异步

        // 使用虚拟线程执行器
        try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {

            // 只对耗时任务使用虚拟线程
            CompletableFuture<UserDetail> detailFuture = CompletableFuture.supplyAsync(() -> {
                try {
                    return getUserDetailInfo(userId);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(e);
                }
            }, virtualExecutor);

            CompletableFuture<UserExtraInfo> extraFuture = CompletableFuture.supplyAsync(() -> {
                try {
                    return getUserExtraInfo(userId);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(e);
                }
            }, virtualExecutor);

            // 并行等待
            UserDetail detail = detailFuture.get();
            UserExtraInfo extra = extraFuture.get();

            User user = new User();
            user.setId(userId);
            user.setName(detail.getName());
            user.setAge(detail.getAge());
            user.setSex(detail.getSex());
            user.setPhone(extra.getPhone());
            user.setLevel(extra.getLevel());

            System.out.println("虚拟线程组装完成,总耗时: " + (System.currentTimeMillis() - startTime) + "ms");
            return user;
        }
    }

压测结果如下:吞吐量再次得到提升

最最最重要的是:上下文切换开销更低:由于虚拟线程在用户空间,而不是通过操作系统,所以它的上下文切换开销更低。

直到压测结束: 接口的耗时时间均为600ms左右,相比之前的传统线程方案,后期的接口响应时间没有增加

虚拟线程组装完成,总耗时: 601ms

虚拟线程组装完成,总耗时: 601ms

虚拟线程组装完成,总耗时: 604ms

虚拟线程组装完成,总耗时: 602ms

虚拟线程组装完成,总耗时: 604ms

虚拟线程组装完成,总耗时: 602ms

虚拟线程组装完成,总耗时: 602ms

虚拟线程组装完成,总耗时: 602ms

虚拟线程组装完成,总耗时: 603ms

虚拟线程组装完成,总耗时: 601ms

虚拟线程组装完成,总耗时: 601ms

Spring WebFlux响应式编程

响应式编程(Reactive Programming)作为一种新的编程范式,解决了传统编程中存在的一些问题,尤其是在高并发和高延迟的场景下。Spring WebFlux 作为 Spring 5 引入的一部分,为开发者提供了一种基于响应式流的编程模型,结合 Reactor 库,能够高效处理异步非阻塞的请求。

参考学习: WebFlux

更改代码如下:

java 复制代码
   @GetMapping("/user/pure-reactive")
    public Mono<User> getUserPureReactive() {
        return userService.assembleUserWebFluxVirtualOptimized();
    }
    @Bean("virtualThreadScheduler")
    public Scheduler virtualThreadScheduler() {
        return Schedulers.fromExecutor(
                Executors.newVirtualThreadPerTaskExecutor()
        );
    }
    @Autowired
    @Qualifier("virtualThreadScheduler")
    private Scheduler virtualThreadScheduler;  // 注入全局调度器

    public Mono<User> assembleUserWebFluxVirtualOptimized() {
        long startTime = System.currentTimeMillis();

        return Mono.fromCallable(() -> getUserBaseInfo())
                .subscribeOn(virtualThreadScheduler)  // 使用注入的调度器
                .flatMap(userId -> {
                    Mono<UserDetail> detailMono = Mono.fromCallable(() -> getUserDetailInfo(userId))
                            .subscribeOn(virtualThreadScheduler);

                    Mono<UserExtraInfo> extraMono = Mono.fromCallable(() -> getUserExtraInfo(userId))
                            .subscribeOn(virtualThreadScheduler);

                    return Mono.zip(detailMono, extraMono)
                            .map(tuple -> {
                                UserDetail detail = tuple.getT1();
                                UserExtraInfo extra = tuple.getT2();

                                User user = new User();
                                user.setId(userId);
                                user.setName(detail.getName());
                                user.setAge(detail.getAge());
                                user.setSex(detail.getSex());
                                user.setPhone(extra.getPhone());
                                user.setLevel(extra.getLevel());

                                System.out.println("优化WebFlux+虚拟线程组装完成,总耗时: " +
                                        (System.currentTimeMillis() - startTime) + "ms");
                                return user;
                            });
                });
    }

压测结果如下: 整体相差不大,可能还需要结合其余的场景进行分析

另外:虚拟线程和WebFlux确实是两种完全不同的并发范式,通常不推荐混合使用

纯webFlux版本:

代码更改:

java 复制代码
   @Bean("customScheduler")
    public Scheduler customScheduler() {
        Scheduler customScheduler = Schedulers.fromExecutor(
                Executors.newFixedThreadPool(200, r -> {
                    Thread t = new Thread(r, "webflux-custom-" + System.currentTimeMillis());
                    t.setDaemon(true);
                    return t;
                })
        );
        return customScheduler;
    }
    @Autowired
    @Qualifier("customScheduler")
    private Scheduler customScheduler;
    public Mono<User> assembleUserCustomSchedulerWebFlux() {
        long startTime = System.currentTimeMillis();

        // 使用自定义的高性能调度器

        return Mono.fromCallable(() -> getUserBaseInfo())
                .subscribeOn(customScheduler)
                .flatMap(userId -> {
                    Mono<UserDetail> detailMono = Mono.fromCallable(() -> getUserDetailInfo(userId))
                            .subscribeOn(customScheduler);

                    Mono<UserExtraInfo> extraMono = Mono.fromCallable(() -> getUserExtraInfo(userId))
                            .subscribeOn(customScheduler);

                    return Mono.zip(detailMono, extraMono)
                            .map(tuple -> {
                                UserDetail detail = tuple.getT1();
                                UserExtraInfo extra = tuple.getT2();

                                User user = new User();
                                user.setId(userId);
                                user.setName(detail.getName());
                                user.setAge(detail.getAge());
                                user.setSex(detail.getSex());
                                user.setPhone(extra.getPhone());
                                user.setLevel(extra.getLevel());

                                System.out.println("自定义调度器WebFlux组装完成,总耗时: " +
                                        (System.currentTimeMillis() - startTime) + "ms");
                                return user;
                            });
                });
    }

吞吐量测试如下:

注意:

如果使用了webFlux后启动的一直是Tomcat,可以加入下面代码:

java 复制代码
    @Bean
    public org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory nettyReactiveWebServerFactory() {
        return new org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory(9000);
    }

缓存优化

JetCache 参考学习: SpringBoot3整合JetCache缓存

在同步的代码上加上缓存注解:

java 复制代码
    @GetMapping("/user/async-ultra")
    @Cached(name="userCache:", expire = 3600, localExpire = 3500, cacheType = CacheType.BOTH)
    public User getUserAsyncUltra() throws Exception {
        return userService.assembleUserVirtualThread();
    }

压测结果如下: 恐怖如斯!!!!

相关推荐
DoubleKK2 小时前
Python 玩转 MCP:在工具函数中获取请求 Headers 的正确姿势
后端
调试人生的显微镜2 小时前
深入理解 iOS 16/17/18/26 描述文件管理:多工具协同下的配置与调试新思路
后端
海边夕阳20062 小时前
PostgreSQL性能调优:解决表膨胀、索引碎片和无效索引问题
数据库·经验分享·postgresql·性能优化
陈果然DeepVersion2 小时前
Java大厂面试真题:Spring Boot+微服务+AI智能客服三轮技术拷问实录(四)
spring boot·redis·微服务·kafka·spring security·智能客服·java面试
一 乐3 小时前
个人理财系统|基于java+小程序+APP的个人理财系统设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·后端·小程序
盖世英雄酱581363 小时前
java深度调试技术【第四五章:多线程和幽灵代码】
java·后端
用户3074596982073 小时前
反射(Reflection)—— PHP 的“元编程之眼”
后端·php
林太白3 小时前
rust13-字典类型
后端·rust
PFinal社区_南丞3 小时前
单文件代码部署工具
后端