当前位置: 代码网 > it编程>编程语言>Java > Spring @Async线程池耗尽与异常处理详解

Spring @Async线程池耗尽与异常处理详解

2026年09月10日 Java 我要评论
1. 从一次线上事故说起假设你负责一个电商系统,用户下单后需要发送通知短信、生成积分流水、更新推荐位。这些操作如果都同步执行,下单接口的响应时间会拖长到好几秒,用户体验很差。于是你决定用 spring

1. 从一次线上事故说起

假设你负责一个电商系统,用户下单后需要发送通知短信、生成积分流水、更新推荐位。这些操作如果都同步执行,下单接口的响应时间会拖长到好几秒,用户体验很差。于是你决定用 spring 的异步能力把这些非核心操作挪到后台执行,让接口先返回“下单成功”。

上线初期一切正常,但到了大促高峰期,下单接口时不时报出类似这样的错误:

task rejected from java.util.concurrent.threadpoolexecutor@xxxx[running, pool size = 5, active threads = 5, queued tasks = 100, completed tasks = 0]

更奇怪的是,有些异步操作的日志消失了,像是根本没执行。打开监控面板,你发现线程池的活跃线程数一直顶在最大值,队列也满了,新任务不断被拒绝。而当你查看业务表时,有些订单的积分没有发放,短信也没发出去,但用户却收到了“下单成功”的反馈。

这就是本文要探讨的核心:spring 的 @async 看着简单,但背后隐藏着线程池配置、任务拒绝、异常处理等一系列风险。如果不在设计阶段就理解清楚,它们会在流量高峰时集中爆发。

本文将带你从问题场景出发,先建立一个可复述的模型,再深入线程池、异常处理、任务拒绝和上下文传递的细节,最后给你一套工程上的决策清单。

2. 一句话模型与整体框架

先记住一个最小模型:调用方把任务交给一个门卫(线程池),门卫决定是让某个工人(线程)立即处理、还是先排到队伍(队列)里,或者直接拒收(拒绝策略)。工人干活时如果出了差错(异常),门卫和工人的沟通方式决定了你能不能知道这个差错。

把整套机制拆成四部分,它们依次关联:

  1. 任务产生者:被 @async 注解的方法调用处,它把任务提交给线程池。
  2. 任务执行容器:线程池(threadpooltaskexecutor),管理线程、队列和拒绝策略。
  3. 任务执行结果:正常返回或抛出异常,异常需要被捕获才能感知。
  4. 任务环境:事务、安全上下文、请求参数等,默认不会自动传递给异步线程。

一次完整的调用流程大致如下:

调用方(main-1线程) → 提交任务 → threadpooltaskexecutor
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        ▼                           ▼                           ▼
  核心线程有空闲                  排队等待                  队列满且线程都忙
  立即执行                       worker线程唤醒               触发拒绝策略
        │                           │                           │
        ▼                           ▼                           ▼
  执行业务逻辑,捕获异常      执行业务逻辑,捕获异常     抛出taskrejectedexception或吞掉

下面我们将沿着这个流程逐步展开:先看线程池如何工作,再深入了解异常处理、任务拒绝和上下文传递的细节。

3. 线程池:@async 的执行基石

3.1 线程池的核心参数与工作原理

spring 的 @async 背后依赖的是 java 的线程池体系。默认情况下,spring 会为 @async 创建一个简单的线程池,但它的参数往往不适合生产环境。我们先明确线程池的几个核心参数,它们决定了线程池的行为:

参数含义示例值
corepoolsize核心线程数,即使空闲也不会回收5
maxpoolsize最大线程数,当队列满且核心线程都忙时才会创建新线程20
queuecapacity等待队列容量,超出后才会尝试创建新线程100
keepaliveseconds非核心线程空闲存活时间60
threadnameprefix线程名前缀,便于日志排查“async-”
rejectionpolicy任务拒绝策略callerrunspolicy

关键规则是:先核心线程 → 再队列 → 再非核心线程 → 最后拒绝。假设核心线程 5,队列容量 100:当提交第 6 个任务时,如果核心线程都忙,任务会进入队列而不是直接创建新线程;只有当队列也满了,才会创建非核心线程来救急,直到达到 maxpoolsize;当线程数达到最大值且队列又满时,新任务才会触发拒绝策略。

很多团队把 maxpoolsize 设得很大,认为越多越好,但实际上每个线程都要占用系统资源,包括栈内存和 cpu 调度成本。线程太多会导致频繁上下文切换,反而降低吞吐量。

3.2 spring 默认线程池如何配置?

如果你只写了一个 @async 注解,没有定义任何线程池,spring boot 会使用默认的 simpleasynctaskexecutor,它不重用线程,为每个任务新建一个线程,这在高并发下几乎必然导致资源耗尽。因此,生产环境中必须自定义线程池。

这里需要纠正一个重要误解:spring 默认并不会自动使用 threadpooltaskexecutor。只有当你显式定义一个 executor 类型的 bean,spring 才会使用它。下面是推荐的做法:

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.scheduling.concurrent.threadpooltaskexecutor;

import java.util.concurrent.executor;

@configuration
public class asyncconfig {

    @bean("taskexecutor")
    public executor taskexecutor() {
        threadpooltaskexecutor executor = new threadpooltaskexecutor();
        executor.setcorepoolsize(5);
        executor.setmaxpoolsize(20);
        executor.setqueuecapacity(100);
        executor.setkeepaliveseconds(60);
        executor.setthreadnameprefix("async-");
        // 拒绝策略:由调用者线程执行任务
        executor.setrejectedexecutionhandler(new threadpoolexecutor.callerrunspolicy());
        executor.initialize();
        return executor;
    }
}

3.3 线程池调优:从业务角度出发

线程池参数的设置不能拍脑袋。需要根据业务特性估算:

  • 任务执行时长:如果任务平均耗时 100ms,那么一个线程每秒能完成 10 个任务。若预期高峰时每秒新增 100 个异步任务,至少需要 10 个线程才能消化。
  • 任务对顺序性的要求:如果任务需要保证顺序执行(比如对某个订单的操作),可能需要为每个订单分配一个队列,技术上是额外问题。
  • 对丢弃任务的容忍度:如果任务允许丢失(如日志),可以配置 discardpolicy;如果不行,必须用缓冲或 callerrunspolicy 降级。

提供一个参考经验值:核心线程数 = cpu 核数 * 目标 cpu 利用率 * (1 + 平均等待/平均计算时间)。对于 io 密集任务,通常是 cpu 核数的 2-4 倍。但最佳实践是压测确定。

4. 核心机制一:@async 的工作过程

4.1 @async 是怎样被激活的?

你需要在配置类或启动类上添加 @enableasync 来激活 spring 的异步处理。这个注解会像开关一样,让 spring 解析 @async 注解并创建代理对象。如果你忘记加这个注解,@async 注解不会生效,方法会同步执行,可能让你误以为异步生效,但实际并未减轻接口压力。

最小示例:

import org.springframework.scheduling.annotation.enableasync;
import org.springframework.context.annotation.configuration;

@configuration
@enableasync
public class appconfig {
    // 其他配置...
}

然后,你只需要在希望异步执行的方法上标明 @async,并确保该方法是被另一个 bean 调用的。

4.2 一个最小可运行的示例:@async 的基础用法

我们先构建一个完整的 spring boot 项目的最小实验,来验证 @async 的行为。

目标:演示 @async 默认使用自定义线程池,并观察调用线程和异步线程的区别。

前置环境:jdk 8+,maven 或 gradle,spring boot 2.7+。

首先创建 spring boot 项目,并添加以下文件:

// pom.xml (依赖的关键部分)
<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-web</artifactid>
</dependency>

配置类:

import org.springframework.context.annotation.configuration;
import org.springframework.scheduling.annotation.enableasync;

@configuration
@enableasync
public class asyncconfig {
    // 若想自定义线程池,在这里定义bean即可
}

异步服务类:

import org.springframework.scheduling.annotation.async;
import org.springframework.stereotype.service;

@service
public class notificationservice {

    @async
    public void sendsms(string phone, string content) {
        system.out.println("发送短信线程: " + thread.currentthread().getname());
        // 模拟耗时操作
        try {
            thread.sleep(1000);
        } catch (interruptedexception e) {
            thread.currentthread().interrupt();
        }
        system.out.println("短信已发送至: " + phone + ", 内容: " + content);
    }
}

控制器:

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.restcontroller;

@restcontroller
public class ordercontroller {

    @autowired
    private notificationservice notificationservice;

    @getmapping("/order")
    public string createorder() {
        system.out.println("下单线程: " + thread.currentthread().getname());
        notificationservice.sendsms("13800138000", "您已成功下单");
        return "订单创建成功";
    }
}

运行后访问 http://localhost:8080/order,观察控制台:

  • 下单线程通常为 http-nio-8080-exec-1(web线程)。
  • 发送短信线程如果没有自定义线程池,默认是 simpleasynctaskexecutor,线程名可能是 simpleasynctaskexecutor-1 等;如果自定义了线程池,则显示你配置的名称前缀,如 async-1

预期输出:

下单线程: http-nio-8080-exec-1
发送短信线程: async-1
短信已发送至: 13800138000, 内容: 您已成功下单

关键理解:方法被 @async 修饰后,调用方线程不会等待方法执行完毕,而是立刻返回。异步方法的执行被交给独立的线程。若没有配置线程池,每次调用都会新建线程,非常浪费。

4.3 注意事项:@async 失效的常见陷阱

  • 自调用(this 调用):如果异步方法在同一个类内部通过 this.xxx() 调用,由于没有经过代理,@async 不生效。例如下面的代码:
@service
public class myservice {
    public void dosomething() {
        // 自调用,@async失效
        asyncmethod();
    }

    @async
    public void asyncmethod() {
        // 异步逻辑
    }
}
  • 方法必须为 public:spring 通过代理实现,private 或 protected 方法不会被代理拦截。
  • 返回值类型限制:异步方法返回类型只能是 voidfuture(包括 completablefuture)。如果返回其他类型,spring 会忽略异步,直接同步执行。

5. 核心机制二:异步异常处理——你抓不到的异常

5.1 为什么异常会“消失”?

先看一个现象:

@async
public void asyncmethodwithexception() {
    throw new runtimeexception("异步任务出错了");
}

调用后,控制台可能打印出错误堆栈,也可能没有(取决于默认配置)。但调用方(如 controller)并不感知这个异常,它也无法在调用点捕获,因为调用已经立即返回了。传统的 try-catch 无法捕获异步任务内部的异常。

原因在于:异常发生在另一个线程中,调用线程根本不知道,也无法直接处理。同时,spring 的 @async 默认将异常记录到日志,但具体是否打印取决于异步执行器是否处理了异常。实际上,spring 在内部会捕获异步方法抛出的异常,如果方法返回类型是 void,它会被 asyncexecutioninterceptor 捕捉并记录到日志,但不会向上传播给调用方;如果方法返回 future,则异常会被封装到 future 中,当你调用 future.get() 时才会抛出。

这里最容易误解的是:以为 catch(exception e)能捕获异步任务异常。在异步场景下,try-catch 包裹的是提交动作,而不是任务执行动作,所以它捕获不到。

5.2 异常处理的第一种:返回 future

通过返回 future,我们可以主动获取任务执行结果和异常。修改一下异步方法:

import org.springframework.scheduling.annotation.asyncresult;
import java.util.concurrent.future;

@async
public future<string> asyncmethodwithreturn() {
    // 若出现异常,会被封装到future中
    if (true) {
        throw new runtimeexception("异步任务出错");
    }
    return new asyncresult<>("成功");
}

调用方可以这样处理:

notificationservice service = ...;
future<string> future = service.asyncmethodwithreturn();
try {
    string result = future.get(); // 会阻塞直到任务完成
    // 处理成功结果
} catch (executionexception e) {
    throwable cause = e.getcause(); // 获取真正的异常
    log.error("异步任务执行失败", cause);
} catch (interruptedexception e) {
    thread.currentthread().interrupt();
}

优点:可以精确获取异常,并选择重试或补偿。缺点:调用方必须等待任务完成,失去了异步的即时性。适合需要关注结果的场景。

5.3 异常处理的第二种:asyncuncaughtexceptionhandler

如果不关心返回结果,只是想统一记录异常,可以注册全局异步异常处理器。只需要实现 asyncuncaughtexceptionhandler 接口,并在配置类中指定。

配置示例:

import org.springframework.aop.interceptor.asyncuncaughtexceptionhandler;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.scheduling.annotation.asyncconfigurer;
import org.springframework.scheduling.annotation.enableasync;

import java.lang.reflect.method;
import java.util.concurrent.executor;

@configuration
@enableasync
public class asyncexceptionconfig implements asyncconfigurer {

    @override
    public executor getasyncexecutor() {
        // 返回你自定义的线程池,可以为null使用默认
        return null;
    }

    @override
    public asyncuncaughtexceptionhandler getasyncuncaughtexceptionhandler() {
        return new asyncuncaughtexceptionhandler() {
            @override
            public void handleuncaughtexception(throwable ex, method method, object... params) {
                system.out.println("异步方法 " + method.getname() + " 抛出未捕获异常:" + ex.getmessage());
                // 这里可以进行告警、记录日志、发送通知等
            }
        };
    }
}

@async 修饰的方法返回 void,且抛出异常时,spring 会调用这个处理器,所有未被捕获的异常都汇聚到这里。这样异常不会丢失,你可以统一格式记录到日志或上报监控。

5.4 异常处理的最佳实践:使用 completablefuture

completablefuture 结合了 future 的异常捕捉和回调机制,更灵活。异步方法返回 completablefuture,可以让异常传播出来,并在调用方里链式处理。

示例:

import org.springframework.scheduling.annotation.async;
import java.util.concurrent.completablefuture;

@service
public class paymentservice {

    @async
    public completablefuture<string> processpayment() {
        return completablefuture.completedfuture("支付成功");
    }
}

调用方可以这样:

service.processpayment()
    .thenapply(result -> result + ", 发积分")
    .exceptionally(ex -> {
        system.err.println("支付处理异常: " + ex.getmessage());
        return "支付失败";
    });

理解:thenapply 链式处理前一步的结果,exceptionally 是异常时的补偿路径。

6. 核心机制三:任务拒绝——池子满了怎么办

6.1 四种拒绝策略对比

当线程池的线程数已达最大值且队列已满,新任务会被拒绝。threadpoolexecutor 提供了四种拒绝策略:

策略行为使用场景
abortpolicy (默认)直接抛出 rejectedexecutionexception任务不能丢弃,想快速感知系统过载
callerrunspolicy调用者线程执行该任务不想丢任务,但可能阻塞调用线程,造成接口变慢
discardpolicy默默丢弃任务允许丢任务,如临时性、可重试任务
discardoldestpolicy丢弃队列中最老的任务,重试被拒绝的任务需要淘汰旧任务来为新任务让路,对新任务友好

默认的 abortpolicy 会让你的异步操作在高峰期直接抛异常,导致业务中断。很多团队会选择 callerrunspolicy,但要注意它会让调用线程执行异步任务,如果异步任务很重,可能导致原本应该快速返回的接口被拖慢,甚至把 web 容器线程池也拖满,变为雪崩。

所以选择时需要考虑:如果任务允许丢弃(比如缓存刷新、非关键数据统计),可以用 discardpolicy;如果任务重要但可以降级(比如发送通知),可以考虑用 discardoldestpolicy(丢弃早任务)或者用 callerrunspolicy 并做好监控。

6.2 拒绝策略的完整示例

让我们用代码演示各种策略的效果。这里我们构造一个会触发拒绝的场景,并对比不同策略的行为。为了演示,我们配置一个极小的线程池,核心线程 1,最大线程 2,队列容量 1,然后提交 5 个任务,每个任务 sleep 几百毫秒。

先定义线程池配置:

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.scheduling.concurrent.threadpooltaskexecutor;
import java.util.concurrent.threadpoolexecutor;

@configuration
public class rejectconfig {

    @bean("smalltaskexecutor")
    public threadpooltaskexecutor smalltaskexecutor() {
        threadpooltaskexecutor executor = new threadpooltaskexecutor();
        executor.setcorepoolsize(1);
        executor.setmaxpoolsize(2);
        executor.setqueuecapacity(1);
        executor.setthreadnameprefix("reject-");
        executor.setrejectedexecutionhandler(new threadpoolexecutor.abortpolicy()); // 通过构造器传入不同的策略
        executor.initialize();
        return executor;
    }
}

任务类:

import org.springframework.scheduling.annotation.async;
import org.springframework.stereotype.component;

@component
public class taskservice {

    @async("smalltaskexecutor")
    public void execute(int i) {
        string t = thread.currentthread().getname();
        system.out.println("开始任务 " + i + ",线程: " + t);
        try {
            thread.sleep(500);
        } catch (interruptedexception e) {
            thread.currentthread().interrupt();
        }
        system.out.println("完成任务 " + i + ",线程: " + t);
    }
}

调用方:

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.restcontroller;

@restcontroller
public class rejectcontroller {

    @autowired
    private taskservice taskservice;

    @getmapping("/submit")
    public string submit() {
        for (int i = 0; i < 5; i++) {
            try {
                taskservice.execute(i);
            } catch (exception e) {
                system.out.println("任务 " + i + " 被拒绝: " + e.getclass().getsimplename());
            }
        }
        return "done";
    }
}

预期观察(使用 abortpolicy 时):

  • 任务 0 被核心线程执行。
  • 任务 1 进入队列。
  • 因为队列满,创建最大线程(第二个线程),任务 2 被新线程执行。此时线程数达到 max=2。
  • 队列里已有任务1,新任务3,线程都已忙,队列也满,任务3被拒绝,抛出 taskrejectedexception(它是 rejectedexecutionexception 的子类)。
  • 任务4也被拒绝。

若换成 callerrunspolicy,则任务3、4会由调用方线程(即 web 线程)执行,不会抛出异常。

这个例子告诉我们:默认的 abortpolicy 在高峰期容易导致误伤;callerrunspolicy 将压力转移到上游。选择哪种策略,需要业务上明确对丢任务的态度。

7. 核心机制四:上下文传递——异步让信息丢失

7.1 为什么需要传递上下文?

在线程切换时,每个线程拥有自己的局部变量、事务、安全上下文(如 securitycontext)、请求参数(如 requestcontextholder)等。这些默认不会共享给异步线程。比如你在 @async 方法中尝试获取当前登录用户,可能会得到 null 或空。事务更是如此,异步方法的事务和调用方不在同一个事务中,甚至没有事务(除非异步方法自身加事务注解,它会在新线程中开启新事务)。

我们为什么要传递上下文? 因为很多业务逻辑依赖请求中的信息,比如订单号、用户 id、traceid 用于日志追踪。如果异步线程拿不到这些,就无法正确关联数据,排查问题也困难。

7.2 spring 提供的便捷方案:taskdecorator

spring 的 threadpooltaskexecutor 支持设置 taskdecorator,它在每次提交任务时对任务进行包装,可以把调用线程的上下文设置到执行线程中去。这是一个常用的解决方案。

下面我们通过复制 requestcontextholder 的请求属性和 mdc 中的 traceid 来做演示。

首先,让我们创建一个自定义的 taskdecorator:

import org.springframework.core.task.taskdecorator;
import org.springframework.web.context.request.requestattributes;
import org.springframework.web.context.request.requestcontextholder;
import java.util.map;
import org.slf4j.mdc;

public class contextcopyingdecorator implements taskdecorator {

    @override
    public runnable decorate(runnable runnable) {
        // 捕获调用线程的上下文
        requestattributes context = requestcontextholder.getrequestattributes();
        map<string, string> contextmap = mdc.getcopyofcontextmap();
        return () -> {
            try {
                // 在异步线程中设置上下文
                requestcontextholder.setrequestattributes(context);
                mdc.setcontextmap(contextmap);
                runnable.run();
            } finally {
                // 清理,避免线程复用导致上下文污染
                requestcontextholder.resetrequestattributes();
                mdc.clear();
            }
        };
    }
}

然后在配置类中启用这个装饰器:

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.scheduling.concurrent.threadpooltaskexecutor;
import java.util.concurrent.executor;

@configuration
public class asyncconfigwithdecorator {

    @bean(name = "contextawareexecutor")
    public executor contextawareexecutor() {
        threadpooltaskexecutor executor = new threadpooltaskexecutor();
        executor.setcorepoolsize(5);
        executor.setmaxpoolsize(20);
        executor.setqueuecapacity(100);
        executor.settaskdecorator(new contextcopyingdecorator());
        executor.setthreadnameprefix("ctx-async-");
        executor.initialize();
        return executor;
    }
}

然后在 @async 中指定使用这个线程池:

@async("contextawareexecutor")
public void sendnotification(order order) {
    // 现在可以获取当前请求的用户信息或mdc中的traceid了
    requestattributes requestattributes = requestcontextholder.getrequestattributes();
    // ...
}

7.3 事务传递:一个不可跨越的边界

关于事务,需要特别注意:调用方的事务边界不会传播到异步线程。如果异步方法中需要数据库操作,每个异步方法都有自己的事务(如果它有 @transactional),或者没有事务。不要指望多个异步操作共享同一个事务。在设计时应尽量让异步任务只做不依赖强一致性的操作,或者用消息队列等异步系统来保证最终一致性。

为什么不能跨线程传播事务? 事务的本质是绑定到线程的数据库连接。spring 的事务管理器把连接放在 threadlocal 中,而异步线程有自己独立的 threadlocal,看不到主线程的连接。强行传播会引起连接管理和事务隔离的复杂性,spring 刻意设计为不跨线程传播。

8. 整体过程:一次业务请求的全景推演

把前面的知识串起来,我们来看一个贴近业务的完整案例。

场景:创建订单成功后,异步执行两个任务:1) 发送短信通知;2) 更新用户的积分。

要求:

  • 发送短信不可失败(至少不抛出异常),如果失败要记录日志。
  • 积分更新成功要提交,失败要能感知。
  • 线程池不能爆,不能因为异步任务把主接口拖垮。
  • 日志中要有关联的 trace-id 和用户 id。

实现方案:

  • 自定义线程池,使用 callerrunspolicy 并配置 queue 大小和核心线程。
  • 自定义 asyncuncaughtexceptionhandler 统一捕获 void 方法异常。
  • 返回 future 的方法让调用方感知异常(积分更新)。
  • 使用 taskdecorator 传递上下文。

关键的代码结构如下:

@configuration
@enableasync
public class asyncconfig implements asyncconfigurer {
    @override
    public executor getasyncexecutor() {
        threadpooltaskexecutor executor = new threadpooltaskexecutor();
        executor.setcorepoolsize(5);
        executor.setmaxpoolsize(15);
        executor.setqueuecapacity(50);
        executor.setthreadnameprefix("order-async-");
        executor.settaskdecorator(new contextcopyingdecorator());
        executor.setrejectedexecutionhandler(new threadpoolexecutor.callerrunspolicy());
        executor.initialize();
        return executor;
    }

    @override
    public asyncuncaughtexceptionhandler getasyncuncaughtexceptionhandler() {
        return (ex, method, params) -> log.error("异步任务执行异常, method: {}", method.getname(), ex);
    }
}

业务 service:

@service
public class orderservice {

    @async
    public void sendnotification(order order) {
        // 发送短信,如果异常则被全局处理器捕获,不会影响主流程
        try {
            smsservice.send(order.getphone());
        } catch (exception e) {
            log.warn("短信发送失败,订到id: {}", order.getid(), e);
        }
    }

    @async
    public completablefuture<void> updatescore(order order) {
        return completablefuture.runasync(() -> {
            // 更新积分,可能抛异常,异常会封装到completablefuture中
            scoreservice.addscore(order.getuserid(), order.getamount());
        });
    }
}

主业务代码中:

public void createorder(order order) {
    // 同步保存订单...
    orderservice.sendnotification(order);
    completablefuture<void> future = orderservice.updatescore(order);
    // 如果需要,可通过future.whencomplete进行结果处理或补偿
    future.whencomplete((result, ex) -> {
        if (ex != null) {
            log.error("积分更新失败,将尝试补偿", ex.getcause());
            // 发送mq进行补偿
        }
    });
    // 返回响应
}

推演一次请求流程:

1. 用户下单请求进入 controller,在 tomcat 线程中执行。
2. 同步保存订单,成功后,调用 orderservice.sendnotification()(提交异步任务)。
   任务先被线程池接收:如果核心线程有空闲,立即执行;否则入队。
3. 几乎同时调用 orderservice.updatescore(),返回 completablefuture。
4. controller 返回响应,主线程释放。
5. 异步线程从队列取任务,执行 sendnotification,成功后继续下一个任务。
6. 若队列满且线程忙,按照 callerrunspolicy,主线程可能亲自执行任务,此时主线程会被阻塞,响应时间变长。
7. 若积分更新抛出异常,该异常被封装在 completablefuture 中,通过 whencomplete 捕获并触发补偿。
8. 若 sendnotification 内部抛出异常,被 asyncuncaughtexceptionhandler 统一记录。
9. 由于 taskdecorator,异步线程可以获得 mdc 中的 traceid,日志能串联起来。

关键成功标准:主接口响应时间不显著增加,异步任务不丢失(除非极端过载),异常可见,日志可追踪。

9. 常见误区与设计取舍

9.1 常见误区速查表

误区真相后果
认为 @async 默认使用自定义线程池spring boot 默认使用 simpleasync(每次新建线程)或单线程池高并发下线程无限增长或只有单线程,吞吐量低
在调用 try-catch 中捕获异步异常异步异常发生在其他线程,调用方无法捕获异常丢失,系统无感知
认为 @async 与 @transactional 可以在同一线程中传播事务事务不跨线程异步方法内如果没有事务,操作处于自动提交模式,容易产生不一致
把 maxpoolsize 设得很大解决并发线程过多导致上下文切换开销性能下降,甚至资源耗尽
忽略 taskdecorator 传递上下文异步线程无法获得请求信息/rdc日志无法关联,业务取不到用户id

9.2 设计取舍:异步还是消息队列?

@async 适用于进程内简单异步,当你需要可靠投递、重试、削峰填谷时,应考虑消息队列(如 rabbitmq、kafka)。两者对比:

维度@async消息队列
投递可靠性内存队列,进程重启可能丢失支持持久化,可靠投递
削峰依赖队列,容量有限可缓冲大量消息
重试机制需自己实现通常自带重试和死信
复杂度简单,侵入式引入外部组件

取舍建议:如果异步任务对一致性要求不高、允许丢失、且并发量可控,用 @async 成本低;如果任务关键,必须不丢,或需要解耦,请选择 mq。

10. 生产实践建议

  1. 永远显式自定义线程池:不要依赖 spring boot 默认行为,自己定义 core、queue、max,并设置有意义的线程名前缀。
  2. 拒绝策略选 callerrunspolicy 还是 discardoldestpolicy:如果任务可以容忍延迟,用 discardoldestpolicy(丢弃最老,保证较新任务),但要注意丢弃逻辑;如果不愿丢任务但可接受主线程阻塞,用 callerrunspolicy,并监控主线程的阻塞增长。建议默认使用 callerrunspolicy,并配合监控告警,当主线程阻塞增多时能快速扩容线程池。
  3. 异步方法返回 void 时,必须配置全局 asyncuncaughtexceptionhandler:将异常日志统一记录并上报。
  4. 需要结果或重试时,返回 completablefuture:用 whencomplete 或 exceptionally 处理异常。
  5. 设置线程池监控:通过 actuator 或自定义 metrics 监控 poolsize、activecount、queuesize、rejectedcount,并设定告警阈值。
  6. 传递上下文:实现 taskdecorator 复制 mdc 和请求上下文,但要注意清理,避免线程池复用造成信息污染。
  7. 不要在异步任务中做大量数据同步:如果有大量数据迁移,评估是否改用批处理或 mq。

11. 排障清单:当异步任务不工作或丢失时

当遇到异步任务没有执行或丢失时,可以按以下顺序排查:

  1. 是否添加了 @enableasync? 在配置类中检查。
  2. 调用方是否通过代理调用? 检查是否同一类内部调用或 final 方法。
  3. 线程池是否撑爆? 查看监控中 rejectedcount 是否增长,或日志是否出现 taskrejectedexception
  4. 异步方法是否返回 void 并抛异常? 查看全局 asyncuncaughtexceptionhandler 有无日志,是否已配置。
  5. 上下文传递是否失败? 在异步方法中打印用户id,检查是否为 null。
  6. 数据库连接池是否足够? 如果异步任务访问数据库,可能因连接池耗尽而阻塞或失败。
  7. 日志中是否有 traceid? 如果没有关联,检查 mdc 是否被传递。

12. 面试/复盘问题

以下问题可用于自查或团队复习:

  1. @async 默认使用什么执行器?为什么不推荐?
  2. 如何捕获 @async 方法返回 void 时的异常?
  3. 如何设计异步任务的上下文传递?
  4. 线程池的饱和策略有哪些?如何选择?
  5. 为什么 @transactional 不会传播到异步线程?你可以如何设计补偿?
  6. 在生产中,你会监控异步线程池的哪些指标?

13. 总结:一张决策清单

核心要点收回:

场景选择
任务允许丢失(日志、监控)使用 discardpolicy 或直接忽略
任务不允许丢失但可延迟使用 callerrunspolicy,并做好主线程监控
需要知道执行结果返回 completablefuture
不需要知道结果,但要统一记录异常实现 asyncuncaughtexceptionhandler
需要传递上下文装饰线程池 taskdecorator
任务重要且量大使用消息队列
并发高且线程池长期满扩容线程池,并评估是否达到瓶颈

以上就是spring @async线程池耗尽与异常处理详解的详细内容,更多关于spring @async线程池耗尽与异常处理的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com