Spring @Async 坑点 + 改造方案

Spring @Async 坑点 + 改造方案

原生 @Async 默认存在的问题

  1. 默认线程池是SimpleAsyncTaskExecutor,不是真正线程池! 它不会复用线程,每来一个任务新建一条线程,高并发下线程疯狂创建,直接 OOM。

很多开发不知道,以为 @Async 自带线程池,线上直接踩大坑。

  1. 没有统一异常捕获 @Async方法如果返回void,任务抛出异常直接静默消失,没有告警,业务完全无感知。 返回 Future 的,异常藏在 Future 对象,不 get 就看不到。
  2. 没有监控、没有线程命名 默认线程名字杂乱,无法埋点监控队列、活跃线程、拒绝计数。
  3. 无法做业务隔离 所有 @Async 共用一套线程,慢任务占满全部线程,其他异步全部饿死。
  4. 事务、上下文丢失 ThreadLocal(用户上下文、traceId 链路追踪)在异步线程丢失,日志看不到链路。

✅生产改造方案:让 @Async 使用我们上面写好的全局线程池

实现AsyncConfigurer,替换 @Async 默认执行器,复用我们写好带afterExecute异常兜底的ThreadPoolExecutor

复制代码
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    // 注入我们上面定义好的全局线程池
    @Resource(name = "globalCommonThreadPool")
    private ThreadPoolExecutor globalCommonThreadPool;

    /**
     * 将@Async默认执行器替换为我们自定义线程池
     */
    @Override
    public Executor getAsyncExecutor() {
        // Spring包装一层,适配@Async接口
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setThreadPoolExecutor(globalCommonThreadPool);
        // 一定要初始化
        taskExecutor.initialize();
        return taskExecutor;
    }

    /**
     * 针对返回void的@Async异步方法,全局未捕获异常处理器
     * 补充afterExecute之外的一层兜底
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            // @Async void方法抛出异常,会走到这里
            log.error("@Async方法执行异常,method:{}",method.getName(),ex);
            // 告警alertService.sendAlert("@Async异步任务异常",ex);
        };
    }
}

⚠️注意: AsyncUncaughtExceptionHandler 只对返回 void 的 @Async 生效 ; 如果 @Async 返回Future<?>,异常依旧封装在 Future,需要afterExecute去解析捕获,两者配合才完整。

📌链路追踪问题(金融系统必处理)

业务系统用 skywalking /sleuth,TraceId放在 ThreadLocal。 @Async 新开线程,主线程 TraceId 丢失,日志串不起来。

解决两种方案:

  1. 包装线程工厂,提交任务的时候把主线程 ThreadLocal 复制到异步线程。

  2. 使用TaskDecorator装饰器。

    //链路复制装饰器示例
    public class TraceTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
    // 拿到主线程上下文
    Map<String,String> context = MDC.getCopyOfContextMap();
    return ()->{
    try{
    if(context != null){
    MDC.setContextMap(context);
    }
    runnable.run();
    }finally {
    MDC.clear();
    }
    };
    }
    }

配置到ThreadPoolTaskExecutor,执行任务前复制 MDC 链路信息,日志 traceId 不中断。

📌业务隔离:给 @Async 指定不同线程池

不同业务不能全部塞同一个线程池,防止饥饿。 我们定义多个线程池 Bean,使用@Async("beanName")指定使用哪个线程池。

复制代码
//使用订单专用线程池
@Async("orderAsyncThreadPool")
public void asyncSaveOrder(){
}
//使用报表专用线程池
@Async("reportAsyncThreadPool")
public void exportReport(){
}

每个独立线程池,全部复用同一套BizThreadFactory + afterExecute异常兜底逻辑。

❗生产编码规范(银行项目代码评审重点)

  1. 禁止不指定 Executor 直接裸用@Async,避免使用默认SimpleAsyncTaskExecutor
  2. void返回的 @Async,不要指望业务内部 try‑catch,依靠两层兜底:AsyncUncaughtExceptionHandler + afterExecute
  3. 异步方法尽量不要依赖 ThreadLocal,如必须,一定要做上下文复制。
  4. 异步任务做好超时控制,内部调用 RPC/DB 必须设置超时,防止任务卡死占住线程。
  5. 重要业务异步任务,不能只靠内存线程池,服务重启任务直接丢失,要结合 MQ 持久化。

🎯面试高频题

Q1:@Async 底层默认线程池是什么?有什么坑?

默认SimpleAsyncTaskExecutor,不做线程复用,每次任务新建线程,高并发下线程暴涨 OOM。生产必须自定义替换为 ThreadPoolExecutor。

Q2:@Async 返回 void 和返回 Future,异常表现有什么不同?

  1. 返回void:异常会交给AsyncUncaughtExceptionHandler
  2. 返回Future:异常封装在 Future 对象,不会触发上面处理器,需要调用 get () 抛出,必须靠线程池 afterExecute 钩子解析 Future 捕获异常

Q3:@Async 线程丢失 TraceId 怎么处理?

通过 TaskDecorator,提交任务时复制 MDC 上下文到异步线程。

Q4:@Async 和直接注入线程池 execute/submit,业务怎么选型?

  1. 简单异步,不需要拿到返回值,业务代码希望简洁,可以用@Async,但是必须替换自定义线程池。
  2. 需要获取返回值、自定义超时、需要手动管控任务提交时机,推荐直接注入线程池,调用 execute/submit。
  3. 核心金融业务,优先直接注入线程池,更可控,代码可读性更强。

🗣️面试口述总结版本

项目中使用 @Async,首先要规避默认 SimpleAsyncTaskExecutor 的坑,这个类不会复用线程,高并发会不断新建线程。我们会实现 AsyncConfigurer 把默认执行器替换为我们自定义的 ThreadPoolExecutor。 针对 void 返回的异步方法,配置 AsyncUncaughtExceptionHandler 做异常兜底;同时线程池重写 afterExecute,处理返回 Future 场景的异常。 另外异步线程会丢失 MDC 链路信息,通过 TaskDecorator 复制上下文保证日志链路完整。不同业务使用不同线程池 bean 做隔离,用 @Async ("beanName") 指定。 对于核心金融业务,为了更强可控性,我们更多直接注入线程池调用 execute/submit,而不是 @Async 注解。

相关推荐
你怎么知道我是队长2 小时前
IPv6 地址完全解析:从结构到实战
开发语言·php
Dovis(誓平步青云)2 小时前
牛奶不能减成负数:冰箱库存的筛选、去重与不可变更新
android·java·开发语言
OPEN-F2 小时前
Python进阶教程:算法与数据结构入门
开发语言·python
薛定猫AI2 小时前
【技术干货】Claude Code多模型代理与反馈闭环:Python实现可验证的AI编程工作流
开发语言·python·ai编程
XR1234567882 小时前
工厂车间无线网络怎么选?AGV 与复杂环境是分水岭
开发语言·php
zq_63892 小时前
利用 MATLAB 调用 STK Object Model COM 显示雷达受干扰前后探测范围的变化
开发语言·matlab
大黄说说2 小时前
类型安全时代:PHP 8+ 的联合类型、交集类型与泛型(模板)最佳实践
开发语言·安全·php
SWAGGY..2 小时前
【C++初阶】:(15)模板进阶--从非类型参数到模板特化与分离编译
java·服务器·前端
小弥儿3 小时前
GPT Image 2 提示词库,把散落的生图提示词变成工程资产
开发语言·javascript·gpt·学习