@Async 用的好好的,突然全变同步执行?自定义线程池 + 三个坑一次讲清
导读
项目里把短信、简历解析这些耗时操作丢给 @Async 异步执行,某天加了自定义线程池配置后,所有异步方法全部退化成同步执行,接口响应时间直接翻倍。排查下来是 @Async 和线程池配置的几个经典坑叠一起了。这篇把这套东西讲透。
基础:@Async 怎么用才对
先看最基础的正确用法——异步方法和调用方不能在同一个类里(自调用会失效,后面细说):
@ServicepublicclassResumeParseService{@Async("asyncExecutor")publicvoidparseResumeAsync(LongresumeId){// 简历解析耗时操作resumeParser.parse(resumeId);}}@RestControllerpublicclassResumeController{@AutowiredprivateResumeParseServiceparseService;@PostMapping("/resume/upload")publicResultupload(@RequestBodyMultipartFilefile){LongresumeId=saveFile(file);// 这里调的是另一个 bean 的方法,代理生效parseService.parseResumeAsync(resumeId);returnResult.ok(resumeId);}}加上@EnableAsync启用,异步就能跑。但注意——一旦你自定义了线程池,很多配置就变了。
自定义线程池:核心参数怎么定
简历解析的异步线程池,我这样配的:
@Configuration@EnableAsyncpublicclassAsyncConfig{@Bean("asyncExecutor")publicThreadPoolTaskExecutorasyncExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(200);executor.setThreadNamePrefix("async-");executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();returnexecutor;}}几个参数的选择逻辑:核心线程 4(机器 4 核,简历解析是 IO 密集型,可以再高些);队列 200(限制任务堆积,防止内存爆掉);拒绝策略 CallerRuns(队列满了让调用线程自己跑,宁可慢一点也不能丢任务)。
坑一:@Async 自调用,异步完全失效
现象:在ResumeService内部一个方法调另一个带@Async的方法,日志里看到异步方法还是同步执行。
原因:Spring 的 @Async 靠代理实现,this 调用不走代理。this.parseAsync()相当于直接调原始对象的方法,注解自然不生效。
解决:异步方法放到独立 Service 里注入调用;或者注入自身代理(@Autowired自己)再调。最简单是拆类。
坑二:异步方法里事务不生效
现象:异步方法里有@Transactional注解,但异常没回滚,数据半截写进去了。
原因:@Async 和 @Transactional 是两套代理,方法被异步代理先拦截,直接调了目标方法(事务代理在更外层才生效),事务压根没挂上。另外异步线程和调用线程不在一个线程,事务的线程绑定也对不上。
解决:事务逻辑放在异步方法内部再调一个事务方法,或者干脆把事务和异步分开——异步方法只做调度,事务方法内部独立:
@ServicepublicclassNotifyService{@Async("asyncExecutor")publicvoidsendNotifyAsync(LongorderId){// 事务方法在另一个 bean 里,代理正常inboxService.saveAndUpdateStatus(orderId);}}坑三:异常被吞,排查全靠日志翻
现象:运营反馈"简历解析明明失败了,页面上却提示成功"。简历解析接口返回 200,但解析服务在后台线程里实际抛了 NPE。
排查:先查接口日志——接口确实是同步返回成功的(异步本来就不阻塞)。再翻解析服务的业务日志,发现完全找不到异常堆栈,只有方法入口和出口的普通日志。这就很奇怪,NPE 不会凭空消失。
定位:@Async 方法抛出的异常默认交给 Spring 的AsyncUncaughtExceptionHandler处理,不配置的话异常直接丢弃,连日志都不打。异步线程和调用线程是两个线程,调用方线程根本接不到异常,接口自然"成功"。
解决:配置全局异常处理器,把异步异常完整打出来:
@Configuration@EnableAsyncpublicclassAsyncConfigimplementsAsyncConfigurer{@OverridepublicAsyncUncaughtExceptionHandlergetAsyncUncaughtExceptionHandler(){return(ex,method,params)->log.error("异步方法执行异常: method={}, params={}",method.getName(),params,ex);}}配完之后,异步异常至少能打到日志里,不至于石沉大海。
可直接复用的清单
- @Async 别在同类内自调用,拆到独立 Bean
- 自定义线程池时**核心线程按 CPU 核数 ×(IO 倍数)**估,队列别塞满内存
- 异步和事务分开,事务方法放独立 Bean
- 必须配
AsyncUncaughtExceptionHandler,否则异步异常全被吞 - 拒绝策略选
CallerRunsPolicy,任务不丢但调用线程兜底执行
这几个坑我前后踩了一个星期,配置改了又改才捋清楚。核心记忆点:@Async 的代理、事务的代理、线程池的配置,三者是独立的,谁也别指望谁自动兜底。