SpringBoot 实战:@Async + CompletableFuture 实现多 SQL 并行统计查询

SpringBoot 实战:@Async + CompletableFuture 实现多 SQL 并行统计查询

一、前言

日常开发中,我们经常遇到多组独立统计查询的业务场景。

如果采用传统串行查询,接口耗时会逐级叠加,高数据量下接口响应极慢、用户体验极差。

本文基于 SpringBoot 最新异步规范,使用 @Async + CompletableFuture 实现多任务并行执行,同时解决开发中 90% 的异步坑点:

  • 同类内部调用 @Async 异步失效问题

  • 自注入导致循环依赖启动报错

  • BigDecimal 高精度计算 Rounding 异常

  • 异步任务异常无兜底、接口崩掉问题

  • JDK 动态代理类型不匹配报错

二、业务场景

业务需要一次性查询6个独立的统计指标 ,指标之间互不依赖,最后通过几何平均算法汇总计算出两个最终评分指标。

特点:

  • 所有统计 SQL 相互独立,无需串行执行

  • 单条统计 SQL 耗时较高,串行叠加严重超时

  • 需要保证计算精度、异常容错、服务稳定

三、传统串行方案痛点

串行执行:总耗时 = 所有 SQL 耗时累加

例如:单条SQL平均500ms,6条就是 3s+,严重超时。

除此之外还有隐性问题:

  1. 代码冗余、执行效率极低

  2. 某一条SQL异常,整体流程直接失败

  3. 无法利用多线程并行能力,服务器资源浪费

四、技术选型:@Async + CompletableFuture

核心优势

  • 并行执行:多任务同时执行,总耗时 = 最慢单任务耗时

  • 线程池托管:Spring 内置线程池,无需手动创建线程

  • 任务统一管理:批量等待、统一汇总、统一异常处理

  • 非阻塞+高并发:适配大数据量统计场景

前置开启异步支持

启动类必须开启异步,同时解决代理类型不匹配问题:

java 复制代码
@SpringBootApplication
@EnableAsync(proxyTargetClass = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

参数说明:

  • @EnableAsync:开启 Spring 全局异步能力

  • proxyTargetClass = true:强制 CGLib 代理,避免 JDK 动态代理类型转换异常

五、核心实现思路(通用脱敏版)

1、接口层规范(关键避坑)

所有异步方法必须在接口中声明,否则代理对象无法调用,出现编译/运行异常。

java 复制代码
public interface DataStatisticsService {
    // 统一入口
    DataStatisticsVo getFullStatistics();

    // 所有异步查询方法声明
    CompletableFuture<BigDecimal> getRate1();
    CompletableFuture<BigDecimal> getRate2();
    CompletableFuture<BigDecimal> getRate3();
    CompletableFuture<BigDecimal> getRate4();
    CompletableFuture<BigDecimal> getRate5();
    CompletableFuture<BigDecimal> getRate6();
}

2、核心异步并行实现(弱化业务、保留通用模板)

核心逻辑:获取代理对象 → 开启多异步任务 → 统一等待执行 → 汇总计算结果

java 复制代码
@Service
public class DataStatisticsServiceImpl implements DataStatisticsService, ApplicationContextAware {

    // 上下文获取代理对象(解决异步失效+循环依赖终极方案)
    private DataStatisticsService self;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.self = applicationContext.getBean(DataStatisticsService.class);
    }

    // 全局统一精度配置,解决Rounding异常
    private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP);

    @Override
    public DataStatisticsVo getFullStatistics() {
        // 1. 并行开启6个异步查询任务
        CompletableFuture<BigDecimal> task1 = self.getRate1();
        CompletableFuture<BigDecimal> task2 = self.getRate2();
        CompletableFuture<BigDecimal> task3 = self.getRate3();
        CompletableFuture<BigDecimal> task4 = self.getRate4();
        CompletableFuture<BigDecimal> task5 = self.getRate5();
        CompletableFuture<BigDecimal> task6 = self.getRate6();

        // 2. 等待所有异步任务执行完成
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        DataStatisticsVo resultVo = new DataStatisticsVo();
        try {
            // 3. 汇总结果、业务计算
            BigDecimal score1 = calculateScore1(getVal(task1), getVal(task2), getVal(task3));
            BigDecimal score2 = calculateScore2(getVal(task4), getVal(task5), getVal(task6), getVal(task1));
            resultVo.setAccuracy(score1);
            resultVo.setConsistency(score2);
        } catch (Exception e) {
            // 全局异常兜底,保证接口不崩
            resultVo.setAccuracy(BigDecimal.ZERO);
            resultVo.setConsistency(BigDecimal.ZERO);
        }
        return resultVo;
    }

    // 以下为异步任务方法(通用模板)
    @Async
    @Override
    public CompletableFuture<BigDecimal> getRate1() {
        return CompletableFuture.completedFuture(/* 自定义查询逻辑 */);
    }

    // 其余异步方法结构一致,省略重复代码
}

3、通用工具方法(解决精度+空值异常)

统一封装,解决 BigDecimal 无限小数、空值、负数、开方异常:

java 复制代码
// 百分比转小数、空值兜底
private static BigDecimal divide100(BigDecimal val) {
    if (val == null || val.compareTo(BigDecimal.ZERO) < 0) {
        return BigDecimal.ZERO;
    }
    return val.divide(new BigDecimal("100"), MC);
}

// 安全开方运算
private static BigDecimal pow(BigDecimal base, double exponent) {
    if (base.compareTo(BigDecimal.ZERO) <= 0) {
        return BigDecimal.ZERO;
    }
    return new BigDecimal(Math.pow(base.doubleValue(), exponent), MC);
}

// 安全获取异步结果
private static BigDecimal getVal(CompletableFuture<BigDecimal> future) {
    try {
        BigDecimal val = future.get();
        return val == null ? BigDecimal.ZERO : val;
    } catch (Exception e) {
        return BigDecimal.ZERO;
    }
}

六、CompletableFuture 核心知识点精讲

1、核心API作用

  • CompletableFuture\&lt;T\&gt;:承载有返回值的异步任务

  • completedFuture\(\):将普通结果包装为异步任务,适配 Spring 异步规范

  • allOf\(\):批量监听多个异步任务,适合多任务并行场景

  • join\(\):阻塞主线程,等待所有任务完成,无受检异常,代码更简洁

  • get\(\):获取异步任务最终执行结果

2、执行流程

  1. 通过代理对象批量开启异步线程

  2. 所有任务并行执行数据库查询

  3. 主线程等待全部任务完成

  4. 统一获取结果、业务计算、封装返回

七、全网高频坑点总结(重点)

坑点1:同类内部调用 @Async 完全失效

原因 :Spring 异步基于 AOP 代理,只有外部调用才会走代理增强,this\.方法\(\) 内部调用直接绕过代理,异步失效。

解决方案 :通过 ApplicationContext 获取代理对象 self,使用 self\.方法\(\) 调用异步方法。

坑点2:自注入 self 导致循环依赖报错

错误写法:@Autowired 自注入自身 Service

终极方案 :使用 ApplicationContext 动态获取 Bean,零循环依赖、最稳定

坑点3:BigDecimal Rounding necessary 报错

原因:无限小数运算未指定舍入模式。

解决:全局统一 MathContext 精度 + 所有 setScale 指定四舍五入模式。

java 复制代码
private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP);

private BigDecimal calculateAccuracy(BigDecimal task1, BigDecimal task2, BigDecimal task3) {

    BigDecimal product = divide100(task1)
            .multiply(divide100(task2), MC)
            .multiply(divide100(task3), MC);
    // 3个指标 → 开3次方 ×100
    return pow(product, 1.0 / 3).multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP);
}

坑点4:异步方法未在接口声明,代码爆红

代理对象基于接口生成,异步方法必须在接口定义,否则找不到方法。

坑点5:异步任务异常导致接口崩溃

封装统一 getVal 工具类,所有异步结果异常/空值强制兜底0,保证服务高可用。

八、性能对比

执行方式 耗时 优缺点
串行执行 3~5s 耗时叠加、极易超时
CompletableFuture 并行 500ms内 只取决于最慢单任务、性能拉满

九、最终总结

1、多独立查询场景优先使用 CompletableFuture 并行,大幅提升接口吞吐量;

2、Spring 异步切记:必须代理调用、禁止内部this调用

3、优先使用 ApplicationContext 获取代理,杜绝循环依赖;

4、高精度财务/统计计算,必须统一精度、统一舍入、统一兜底;

5、异步编程一定要做异常兜底,避免单任务异常导致整体业务失败。

相关推荐
weixin_456723161 小时前
Java项目的rabbitmq配置vhost
java·rabbitmq·java-rabbitmq
李少兄1 小时前
解决 java.net.ConnectException: Connection refused 报错
java·开发语言·.net
-南帝-1 小时前
行尾符格式转换问题(CRLF vs LF)如何快速解决(Agent)
java·ai
小新同学^O^1 小时前
算法学习 --> 快速输入和输出
java·学习·算法
喜欢小苹果的码农1 小时前
Java动态多定时任务
java
Gauss松鼠会1 小时前
浅谈GaussDB (DWS)技术【玩转PB级数仓GaussDB(DWS)】
数据库·经验分享·sql·数据库开发·gaussdb·经验总结
无所事事O_o1 小时前
基于netty的websocket服务优化
java·websocket·netty·优化
有趣灵魂2 小时前
Java Spring Boot根据Word模板和动态数据生成Word文件
java·spring boot·word·apache