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. 一句话模型与整体框架
先记住一个最小模型:调用方把任务交给一个门卫(线程池),门卫决定是让某个工人(线程)立即处理、还是先排到队伍(队列)里,或者直接拒收(拒绝策略)。工人干活时如果出了差错(异常),门卫和工人的沟通方式决定了你能不能知道这个差错。
把整套机制拆成四部分,它们依次关联:
- 任务产生者:被
@async注解的方法调用处,它把任务提交给线程池。 - 任务执行容器:线程池(
threadpooltaskexecutor),管理线程、队列和拒绝策略。 - 任务执行结果:正常返回或抛出异常,异常需要被捕获才能感知。
- 任务环境:事务、安全上下文、请求参数等,默认不会自动传递给异步线程。
一次完整的调用流程大致如下:
调用方(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 方法不会被代理拦截。
- 返回值类型限制:异步方法返回类型只能是
void或future(包括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. 生产实践建议
- 永远显式自定义线程池:不要依赖 spring boot 默认行为,自己定义 core、queue、max,并设置有意义的线程名前缀。
- 拒绝策略选 callerrunspolicy 还是 discardoldestpolicy:如果任务可以容忍延迟,用
discardoldestpolicy(丢弃最老,保证较新任务),但要注意丢弃逻辑;如果不愿丢任务但可接受主线程阻塞,用callerrunspolicy,并监控主线程的阻塞增长。建议默认使用callerrunspolicy,并配合监控告警,当主线程阻塞增多时能快速扩容线程池。 - 异步方法返回 void 时,必须配置全局 asyncuncaughtexceptionhandler:将异常日志统一记录并上报。
- 需要结果或重试时,返回 completablefuture:用 whencomplete 或 exceptionally 处理异常。
- 设置线程池监控:通过 actuator 或自定义 metrics 监控 poolsize、activecount、queuesize、rejectedcount,并设定告警阈值。
- 传递上下文:实现 taskdecorator 复制 mdc 和请求上下文,但要注意清理,避免线程池复用造成信息污染。
- 不要在异步任务中做大量数据同步:如果有大量数据迁移,评估是否改用批处理或 mq。
11. 排障清单:当异步任务不工作或丢失时
当遇到异步任务没有执行或丢失时,可以按以下顺序排查:
- 是否添加了 @enableasync? 在配置类中检查。
- 调用方是否通过代理调用? 检查是否同一类内部调用或 final 方法。
- 线程池是否撑爆? 查看监控中 rejectedcount 是否增长,或日志是否出现
taskrejectedexception。 - 异步方法是否返回 void 并抛异常? 查看全局
asyncuncaughtexceptionhandler有无日志,是否已配置。 - 上下文传递是否失败? 在异步方法中打印用户id,检查是否为 null。
- 数据库连接池是否足够? 如果异步任务访问数据库,可能因连接池耗尽而阻塞或失败。
- 日志中是否有 traceid? 如果没有关联,检查 mdc 是否被传递。
12. 面试/复盘问题
以下问题可用于自查或团队复习:
@async默认使用什么执行器?为什么不推荐?- 如何捕获
@async方法返回 void 时的异常? - 如何设计异步任务的上下文传递?
- 线程池的饱和策略有哪些?如何选择?
- 为什么
@transactional不会传播到异步线程?你可以如何设计补偿? - 在生产中,你会监控异步线程池的哪些指标?
13. 总结:一张决策清单
核心要点收回:
| 场景 | 选择 |
|---|---|
| 任务允许丢失(日志、监控) | 使用 discardpolicy 或直接忽略 |
| 任务不允许丢失但可延迟 | 使用 callerrunspolicy,并做好主线程监控 |
| 需要知道执行结果 | 返回 completablefuture |
| 不需要知道结果,但要统一记录异常 | 实现 asyncuncaughtexceptionhandler |
| 需要传递上下文 | 装饰线程池 taskdecorator |
| 任务重要且量大 | 使用消息队列 |
| 并发高且线程池长期满 | 扩容线程池,并评估是否达到瓶颈 |
以上就是spring @async线程池耗尽与异常处理详解的详细内容,更多关于spring @async线程池耗尽与异常处理的资料请关注代码网其它相关文章!
发表评论