方法上已经加了 @Async,请求还是等着它做完。看日志,方法里外打印的线程名也一样。先把调用链找出来,看这次调用有没有经过 Spring 的异步代理。
下面按 Spring Boot 3.5、Spring Framework 6.2、JDK 17 或 21 来排查,使用默认的代理模式。先在调用前和异步方法入口各打一条日志,带上线程名。方法耗时只能说明它慢,线程名能帮助确认它实际跑在哪。 Spring Framework 异步调用说明
先找谁调用了这个方法
常见的路径是同一个 Service 里,submit() 直接调用 this.generateReport()。这种内部调用会绕过代理,generateReport() 上的 @Async 也就没有机会拦截这次调用。把异步方法放到另一个 Spring Bean,通过注入的对象调用,再看一次线程名。 Spring 对 self invocation 的说明
同时确认启用异步的配置确实被加载了,类里有 @EnableAsync,任务对象也由 Spring 管理。如果调用的是自己 new 出来的对象,Spring 配置的异步行为不会自动加到它身上。排查时可以先用一个 public、非 final 的方法做探针,把对象来源和调用方式固定下来。 启用异步注解支持
用一个带名字的执行器做探针
在带有 spring-boot-starter-web 的本地测试项目里,把下面三个文件放进启动类能扫描到的包。示例用 com.example.demo.asyncprobe,假设启动类在 com.example.demo 包下。它只打印线程名,确认后可以删掉探针类。
先定义一个小执行器。这里的一条工作线程和八个排队位置,方便观察这次调用。线程数和队列大小用于这个练习,业务配置仍要按任务耗时、并发和下游承受能力来定。
AsyncProbeConfig.java
package com.example.demo.asyncprobe;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncProbeConfig {
@Bean("probeExecutor")
public ThreadPoolTaskExecutor probeExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(8);
executor.setThreadNamePrefix("probe-");
return executor;
}
}
再把工作方法放进独立的 Bean。@Async("probeExecutor") 对应上面 Bean 的名字,后面找线程池时就能沿着这个名字查。 @Async 的执行器限定
ProbeWorker.java
package com.example.demo.asyncprobe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class ProbeWorker {
private static final Logger log = LoggerFactory.getLogger(ProbeWorker.class);
@Async("probeExecutor")
public void record() {
log.info("worker thread={}", Thread.currentThread().getName());
}
}
最后从另一个 Bean 调用它。这里用一个 Controller,让调用方式和普通 HTTP 请求一致。
ProbeController.java
package com.example.demo.asyncprobe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProbeController {
private static final Logger log =
LoggerFactory.getLogger(ProbeController.class);
private final ProbeWorker worker;
public ProbeController(ProbeWorker worker) {
this.worker = worker;
}
@GetMapping("/debug/async-probe")
public String probe() {
log.info("caller thread={}", Thread.currentThread().getName());
worker.record();
return "submitted";
}
}
启动后,从本机访问一次。下面假设 Web 端口为 8080,应用沿用根路径,这个本地请求也已被访问策略允许。
curl -fsS http://127.0.0.1:8080/debug/async-probe
把 caller 和 worker 两条日志并排看。工作线程应带 probe- 前缀。请求线程的名字随 Web 容器和线程配置而变化,日志的先后顺序也会受线程调度影响。
探针能换线程,原业务仍在当前线程时,就对照两边的调用来源、Bean 是否受管、异步配置和执行器名称。已有 AsyncConfigurer 或自定义执行器的项目,也把这些配置一起看。 Spring Boot 的任务执行器配置
只有流量大时回到请求线程,就看拒绝策略
如果低流量时工作在线程池里执行,高流量时却回到了提交任务的线程,检查执行器是否设置了 CallerRunsPolicy。这项策略会让提交者在池饱和时执行任务,前提是执行器还没有关闭。对 HTTP 请求来说,提交者就可能是请求线程。 JDK 的 CallerRunsPolicy 文档
这时记录活动线程数、队列长度和任务耗时,再看哪里开始积压。 Java 线程池打满、任务排队怎么查
线程换了,请求仍然很慢
再找调用方有没有马上 get() 或 join() 等待返回的 Future。它们会把调用方留在等待里。业务如果必须等任务结果,异步提交本身不会消掉这段等待。
@Async 方法支持 void 或 Future 类型的返回值,CompletableFuture 也属于这里。返回 Future 的方法,异常可以随结果被调用方观察。void 方法的异常则需要看异步异常处理器和日志。排查时把“任务提交了”“开始跑了”“业务做完了”分别留出可观察的证据。 @Async 返回值约定 · Spring 的异步异常处理
异步任务里还要写数据库时,继续确认工作线程中的事务边界。 Spring Boot @Transactional 不回滚怎么排查
Spring Boot 的 @Async 怎么还在当前线程跑
https://java.li/archives/spring-boot-async-not-working
评论