ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot异步调用实战:@Async线程池、WebFlux与消息队列全解析

SpringBoot异步调用实战:@Async线程池、WebFlux与消息队列全解析

1. 为什么要做异步调用:从一次接口超时说起

先讲一个真实的场景。之前我给某个后台管理系统做导出功能,用户在前端点了一个“导出报表”,后端同步去数据库里查几十万条数据,再拼装Excel,最后返回文件流。单次执行时间普遍在15秒以上,前端请求早超时了,用户反复点了几次,后台直接堆了好几个线程在处理同一件事。

这个问题的根源不在代码性能,而是同步阻塞的调用模型。请求线程从接收到返回,全程霸占着不放,导出的耗时越长,能用线程就越少,数据库连接池、Tomcat线程池都被拖累,服务整体吞吐跟着掉。

SpringBoot下的异步调用,就是面向这类场景的标准解法:把耗时操作丢给另一个线程去执行,请求线程立刻返回,调用方不需要干等。本文会从三种主流实现方式展开:基于@Async注解的线程池异步、基于WebFlux的响应式异步、以及消息队列的最终解耦,同时把实际开发中容易踩的失效、线程耗尽、上下文丢失这几个坑一并讲清楚。

2. SpringBoot异步调用整体方案盘点

2.1 同步、异步、阻塞、非阻塞几个概念先理清

聊异步之前,得先把概念统一一下,不然下面代码容易看混。

同步和异步,说的是调用方的处理方式。调一个方法,必须等它跑完才能继续做别的事,就是同步;调用后立即返回,结果通过回调、轮询、Future或事件来获取,就是异步。

阻塞和非阻塞,说的是线程的状态。线程在等待结果期间被挂起、不能做别的事,就是阻塞;线程发起操作后不等待,继续往下执行,就是非阻塞。

“异步”不等于“快”,它只是让当前线程不被拖住。真正省时间的是并行的计算资源,所以异步方案几乎必须配套线程池,否则异步任务全堆在单线程里,反而更慢。

2.2 三套方案怎么选

方案核心机制适合场景需要注意的点
@Async + 线程池AOP代理 + 独立线程池执行方法业务代码里局部耗时的操作,比如发通知、写日志、生成报表自调用无效、线程池参数要配好
WebFlux 响应式异步基于Netty事件循环 + 响应式流高并发IO密集型接口,网关层、数据聚合接口全链路要反应式,不能混用阻塞调用
消息队列异步生产者发消息,消费者独立处理跨系统解耦、削峰填谷、可靠投递场景需要额外中间件,要考虑消息幂等

这三套方案不是互相替代的关系。一个大型项目里,接口内部会用@Async处理局部任务,核心入口用WebFlux扛流量,系统之间再用消息队列解耦,各管一段。

3. @Async注解:最常用的异步实现方式

3.1 第一个Demo:从配置到生效

@Async是Spring框架自带的能力,SpringBoot里用起来很方便。先在启动类或者配置类上加上@EnableAsync,意思是开启异步调用的注解扫描:

@SpringBootApplication @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

然后写一个异步服务,把需要异步执行的方法用@Async标记:

@Service public class NotificationService { @Async public void sendSms(String mobile, String content) { // 模拟短信接口耗时 try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("短信发送完成: " + mobile); } }

调用方正常注入Service,调用方法就行:

@RestController @RequestMapping("/api/order") public class OrderController { private final NotificationService notificationService; public OrderController(NotificationService notificationService) { this.notificationService = notificationService; } @PostMapping("/create") public String createOrder() { // 订单业务逻辑 notificationService.sendSms("138****1234", "您的订单已创建"); return "下单成功"; } }

运行起来,请求接口后响应立刻返回,不会等那3秒。短信任务在独立的线程里执行,这就是最基础的@Async用法。

3.2 为什么Async能生效:代理机制要明白

这是理解@Async的关键点,也是后续排查失效问题的理论基础。

Spring容器在启动时,扫描到@Async注解的方法,会给Bean生成一个代理对象。当我们从容器里拿到这个Bean并调用方法时,实际调用的是代理对象,代理对象会先把任务提交给线程池,然后返回。

也就是说,异步逻辑是被“包装”进去的,而不是你写的那个方法本身变成了异步。

基于这个原理,有两个经典结论:

第一,@Async方法必须被Spring容器管理,也就是放在@Service、@Component这些Bean里。

第二,同一个类内部调用带@Async的方法,不会走代理,等于直接调用原方法,异步失效。后面第6部分我会专门讲这个坑。

3.3 自定义线程池:让异步任务跑在正确的线程上

默认情况下,@Async用的是Spring自带的SimpleAsyncTaskExecutor,这个类每次执行都会新建一个线程,没有复用机制。高并发场景下线程数无上限增长,系统资源很快耗尽。生产环境必须自定义线程池。

推荐用Java配置类创建ThreadPoolTaskExecutor:

@Configuration public class AsyncConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数:正常情况下常驻的线程数量 executor.setCorePoolSize(10); // 最大线程数:高峰期最多能扩展到的线程数量 executor.setMaxPoolSize(50); // 队列容量:核心线程全忙时,新任务先放队列 executor.setQueueCapacity(200); // 线程名前缀:排查问题时好识别 executor.setThreadNamePrefix("async-task-"); // 拒绝策略:队列也满的时候怎么处理新任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }

参数大小怎么定,这是被问得最多的。核心线程数参考机器的CPU核数和任务类型。IO密集型任务(调远程接口、读写文件)等待时间占比高,核心线程数可以设成CPU核数的2倍以上;CPU密集型任务(大量计算)设成CPU核数加1到2个就够。

设核心线程10、最大线程50、队列200,意思是正常情况下10个线程在跑,任务多的时候先排队,队满之后才会把线程数往上提,最多50个,还是处理不过来就触发拒绝策略。

使用自定义线程池时,@Async注解里要指定名字:

@Async("taskExecutor") public void sendSms(String mobile, String content) {

如果不指定,Spring会按类型查找默认执行器,找到唯一实现类就自动使用。建议还是显式写名字,多个线程池并存时不容易混。

3.4 有返回值的异步调用:Future与CompletableFuture

现实中很多异步任务需要带结果回来。@Async支持Future类型的返回值,最常用的是CompletableFuture。

@Service public class ReportService { @Async("taskExecutor") public CompletableFuture<String> generateReport(Long userId) { // 耗时逻辑,比如统计数据 String reportContent = "用户" + userId + "的报表数据..."; return CompletableFuture.completedFuture(reportContent); } }

调用方可以主动组合多个异步任务:

public String handleReportRequest() throws Exception { CompletableFuture<String> reportFuture = reportService.generateReport(1001L); CompletableFuture<String> otherFuture = otherService.generateOtherData(1002L); // 等待两个任务都完成,并合并结果 String result = CompletableFuture.allOf(reportFuture, otherFuture) .thenApply(v -> reportFuture.join() + " + " + otherFuture.join()) .get(30, TimeUnit.SECONDS); return result; }

allOf会等所有任务完成后再执行合并逻辑,get方法设置超时时间,避免任务卡死导致调用方无限等待。

这里有一个细节:CompletableFuture.join和get的区别。get抛受检异常,需要try-catch或向上抛出;join不抛受检异常,直接在RuntimeException里包装。业务代码里我更习惯用get带超时,至少不会把控制权无限交给异步任务。

3.5 异步任务的异常处理

@Async方法默认的异常处理方式是:如果方法没有返回值,异常会被吞掉,日志里可能什么都看不到。这是新手最容易忽略的点。

处理方案有两种。

第一种:方法返回Future,异常会封装在Future里,调用get时抛出,由调用方处理。这种方式能让异常回到业务链路,但调用方需要等待结果,有些场景不算理想。

第二种:实现AsyncUncaughtExceptionHandler,配置成自定义的异步异常处理器:

@Configuration public class AsyncExceptionConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(200); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> { // 这里统一记录异常日志,或者接入告警系统 System.err.println("异步方法执行异常: " + method.getName()); ex.printStackTrace(); }; } }

注意:如果实现AsyncConfigurer,getAsyncExecutor里返回的Executor就是全局默认的线程池,所有没指定名称的@Async都会用它。如果类里还有其他配置方式,要避免两个配置源互相覆盖。

异常处理选哪种,取决于对这个异步任务结果的依赖程度。纯通知类任务用自定义Handler记录异常就够了;核心流程的分支任务,建议返回Future,让主流程能感知失败。

3.6 @Async与事务的叠加效果

@Async和@Transactional同时使用时,很多人以为事务会正常工作,实际上需要分情况。

Spring的事务也是通过代理实现的,@Transactional默认只在当前线程内生效。@Async方法把逻辑推到了另一个线程,事务注解如果只写在异步方法上,事务还是有效的——只要事务管理器能拿到对应的连接。

但组合时的经典问题是事务传播。假设主方法开启事务,调用本类里一个被@Async标记的方法,这个方法又写了@Transactional,此时它跑在另一个线程里,主线程的事务上下文传不过去,两个事务互相独立。如果异步方法内部对同一数据做了修改,主事务提交失败时,异步事务可能已经提交了,数据一致性就出问题了。

实操建议:把异步和事务当成两个独立的边界。事务管理交给主流程,异步方法里的数据一致性靠消息确认、状态机或者定时补偿来解决,不要指望跨线程的两个代理还能保持完整的ACID语义。

4. WebFlux响应式异步:换一种执行模型

4.1 WebFlux不是“异步版的SpringMVC”

有些人把WebFlux理解成“用起来更异步的MVC框架”,这是不对的。WebFlux改变了底层的执行模型。

SpringMVC基于Servlet规范,每个请求占用一个Tomcat工作线程,线程阻塞时这个线程什么都不能干。WebFlux基于Netty事件循环,用的是少量线程配合事件回调来处理海量并发请求。线程不阻塞等待,而是注册回调,数据来了再触发后续逻辑。

所以WebFlux的高并发能力,来自它极低的线程占用。同样一台机器,MVC能扛几百并发时,WebFlux往往能扛几千甚至更高,前提是代码里不能有阻塞调用。

引入WebFlux,在pom里加依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>

注意:如果项目里同时引入了spring-boot-starter-web(MVC),要移除其中一个,或者明确端口配置,否则两个容器会冲突。

4.2 响应式写法的基本形态

接口返回Mono或Flux类型,表示这个结果是“未来某个时刻才会出现的”:

@RestController @RequestMapping("/api/user") public class UserReactiveController { private final UserService userService; public UserReactiveController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public Mono<User> getUser(@PathVariable Long id) { return Mono.fromCallable(() -> userService.findById(id)) .subscribeOn(Schedulers.boundedElastic()); } }

这个接口里,findById这个阻塞型操作被丢到boundedElastic线程池里执行,而Netty事件循环线程立刻返回,不阻塞。等数据查到了,再通过回调把响应写回客户端。

4.3 响应式链路的写法规范

响应式异步的关键在于整条链路都要保持响应式。

如果Service层用的是JPA的findById,这本身就是阻塞调用。把它包在Mono.fromCallable里的效果是:异步线程执行了阻塞查询,这确实能提升并发,但底层依赖的还是线程池,本质上是“用线程池隔离阻塞”而不是真正的全链路响应式。

真正做到全链路响应式,要求数据访问层也返回响应式类型。比如用R2DBC访问数据库:

public interface UserRepository extends ReactiveCrudRepository<User, Long> { Flux<User> findByStatus(String status); } @RestController public class UserController { private final UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("/active") public Flux<User> getActiveUsers() { return userRepository.findByStatus("ACTIVE"); } }

这种写法下,从HTTP入口到数据库驱动,全程没有阻塞等待。每个环节都是“准备好了就通知下一个环节”,线程利用率极高。

4.4 实际项目里的语句

WebFlux适合网关层、聚合层,比如给App提供统一的聚合接口,同时调用订单服务、支付服务、用户服务的接口,把它们的结果合并返回。每个下游调用都返回响应式结果,合并时用zip:

public Mono<OrderDetailVO> getOrderDetail(String orderId) { Mono<Order> orderMono = orderClient.getOrder(orderId); Mono<List<Payment>> paymentMono = paymentClient.getPayments(orderId); Mono<User> userMono = userClient.getUserByOrderId(orderId); return Mono.zip(orderMono, paymentMono, userMono) .map(tuple -> { Order order = tuple.getT1(); List<Payment> payments = tuple.getT2(); User user = tuple.getT3(); return buildVO(order, payments, user); }); }

三个下游调用并行执行,全部返回后组装成VO发给客户端。如果是MVC写法,这种聚合要么串行请求,要么手动管理线程池,代码冗长且容易出错。

4.5 WebFlux的代价

响应式不是免费的午餐。它最大的问题是调试困难。传统代码的堆栈在线程切换后变得支离破碎,异常信息经常不会指向真正出问题的位置。响应式代码的学习曲线也陡峭,flatMap、concatMap、switchIfEmpty这些操作符的语义,不熟悉的人很容易写出性能极差的代码。

我的个人结论是:中小型项目,业务逻辑复杂,团队也没有响应式经验,不要强行上WebFlux。用@Async配合线程池改造局部痛点,性价比高得多。WebFlux适合高并发、IO密集、业务链路清晰的下游聚合或网关项目。

5. 消息队列:跨系统异步解耦

5.1 什么时候需要消息队列

@Async解决的是进程内的异步,消息队列解决的是跨进程的异步。订单创建后要通知积分系统、短信平台、物流系统,如果通过RPC同步调用,任何一个下游出问题都会拖累下单主流程,而且下游流量波动还会反过来挤压订单服务的资源。

消息队列的方案是:订单服务把“订单创建完成”这个事件以消息的形式写入队列,下单请求到这里就结束了。积分服务、短信服务分别订阅这个主题,各自来消费。订单服务不再依赖下游的可用性,下游也通过订阅的方式解耦了与订单服务的强绑定。

流程:

  1. 订单服务作为Producer,发送消息到Topic
  2. 消息队列Broker持久化消息,确认收到
  3. 积分服务、物流服务作为Consumer订阅Topic
  4. 各自消费消息,更新自己的数据

5.2 消息队列和@Async怎么配合

消息队列两侧都会用到@Async。

Producer侧,发送消息本身是快速操作,但网络异常重试时可能耗时,所以发送逻辑可以用@Async异步化,避免阻塞业务主流程。Consumer侧,如果一次拉取到多条消息,单条消息处理耗时较长,可以在消费者内部再用@Async把每条消息的业务处理丢到线程池并发执行,提升消费吞吐。

要注意的是消费端的并发度控制。如果消费者线程池开得太大,消息处理太快,可能压垮下游数据库。一般建议消费线程池的最大线程数跟数据库连接池大小挂钩,保证线程数不超过连接数的上限。

5.3 消息队列的可靠性问题

消息队列把同步调用变成了异步通知,同时也引出了一个新的问题:怎么保证消息不丢、不重、不乱序。

不丢:Producer发送时开启确认机制,Broker持久化完成后再返回成功;Consumer消费后处理完业务再提交位点。

不重:消费端要做幂等,因为消息队列的语义是“至少一次”,重复消费是正常现象。常见做法是消费端先查本地状态,已经处理过的直接返回成功,或者用唯一业务ID去重。

不乱序:同一业务的消息要保证分区键一致,让它们进同一个分区,消费者单线程消费这个分区。

这部分的坑主要集中在幂等上。很多项目刚开始只做了“不丢”,没做“不重”,线上跑一段时间就发现重复数据。做消息队列异步,第一件要设计好的事就是幂等方案,而不是消息发送本身。

6. 三大高阶问题:失效、线程耗尽、上下文丢失

6.1 @Async失效的几种常见场景

熟悉了代理原理之后,失效问题基本都能自己排查。最常见的失效场景有四种:

第一种,方法通过this调用。比如一个Service里面,methodA调用methodB,methodB标了@Async。因为this指的是原始对象而不是Spring代理,所以@Async不生效。解决办法:注入自身代理,或者把异步方法拆到另一个Service里。

第二种,异步方法不是public。Spring的代理默认只能拦截public方法,private和protected方法不会进入代理逻辑。

第三种,异步方法所在Bean没有被Spring管理。比如自己new出来的对象,Spring根本不知道它的存在。

第四种,配置类上没有@EnableAsync。这个看起来低级,但项目里多模块化之后,经常出现主启动类扫描不到配置类的情况。

6.2 线程池被打满的排查方法

异步任务量大时,线程池可能被打满,任务进队列,队列也满了,触发拒绝策略。CallerRunsPolicy会把这个任务交给调用方线程执行,看起来是“降级”,但调用量巨大时会把请求线程拖死。

排查时主要靠日志。所以自定义线程池时setThreadNamePrefix非常重要:

executor.setThreadNamePrefix("async-notify-");

日志里搜async-notify线程,能看到线程数是否接近maximumPoolSize,队列深度是否持续增长。如果队列长期堆积,说明核心线程数偏低或者任务量超过预期,要调整参数,而不是无限加线程。

另外一个容易忽略的点是:如果任务里都有数据库查询,线程池配置不能只看服务本身的指标,还要看数据库连接池的可用连接数。线程数设50,数据库连接池默认只有10,50个线程同时查库,10个持有连接,剩下40个在等待,这比不异步还要糟糕。线程池参数必须与会话外资源联动评估。

6.3 异步线程里丢失请求上下文

@Async方法里需要拿到当前登录用户信息、请求头、traceId时,经常发现是null。这是因为请求线程的数据存在ThreadLocal里,而异步任务跑在不同的线程上,ThreadLocal不共享。

常用解决办法是装饰器模式:主线程在提交任务前,把关键上下文拷贝一份,传给目标线程。

Spring提供了TaskDecorator接口,可以优雅地实现上下文传递:

@Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(200); executor.setThreadNamePrefix("async-"); // 任务提交时,将主线程的上下文复制到子线程 executor.setTaskDecorator(runnable -> { // 实际项目中这里会把请求上下文对象暂存 Map<String, String> contextMap = copyContextFromCurrentThread(); return () -> { try { setContextToCurrentThread(contextMap); runnable.run(); } finally { clearContext(); } }; }); executor.initialize(); return executor; }

如果是Spring Cloud链路追踪的场景,也可以用类似方式把traceId从主线程传到异步线程,保证全链路日志能串起来。

具体到逻辑执行,这段decorator是为了展示结构,实际会把traceId、用户信息请求上下文做成一个线程私有对象。

6.4 异步事务隔离与资源共享

异步任务共享数据库连接池、Redis连接池时,要考虑资源竞争问题。之前帮别人排查过一个线上故障:异步线程池核心线程从5调到了20,数据库连接池没动,结果每个异步任务里都有大约300ms的数据库查询,20个线程同时查询,连接池不够用,主业务的请求也开始等连接,整个服务响应变慢。

这类问题的排查步骤是:

  1. 看监控里线程池活跃线程数
  2. 看数据库连接池活跃连接数
  3. 看两者是否存在明显的共同增长趋势
  4. 计算单个任务的平均IO时间,反推需要的并发度

不要盲目调高线程池,资源配置是联动的关系。

7. 常见问题速查与避坑清单

下面这个表是根据我在项目中遇到过的真实情况整理的,方便直接对照排查。

问题可能原因解决思路
@Async方法同步执行this自调用、方法非public、Bean未被Spring管理拆Service、改public、确认包扫描
异步异常无日志输出方法无返回Future且未配置异常处理器自定义AsyncUncaughtExceptionHandler
线程数暴增使用了默认SimpleAsyncTaskExecutor自定义线程池并指定名称
队列任务堆积核心线程数过少或任务量突增压测后调参,结合队列容量一起评估
异步方法内用户信息为空ThreadLocal无法跨线程传递使用TaskDecorator拷贝上下文
接口偶尔变得很慢异线程池触发了拒绝策略CallerRunsPolicy监控线程池指标,动态调参
异步事务数据不一致跨线程事务隔离导致主流程管事务,异步靠补偿
WebFlux接口里用了Thread.sleep阻塞了事件循环线程改为非阻塞延时或换到boundedElastic

补充几个容易忽略的实践细节:

每个异步任务都要有业务标识。日志里把业务ID、用户ID、traceId全部打出来,排查问题的时候能直接定位到是哪个任务、哪个线程在跑。

线程池参数要支持动态调整。生产环境用配置中心管理核心线程数、最大线程数,压测发现问题后可以在不重启服务的情况下修改,效果比改代码快很多。

异步任务要有熔断或者降级方案。线程池满时的拒绝策略不要只选一种,结合业务的容忍度来选。可以丢弃不重要任务(DiscardPolicy),也可以让调用方自己跑(CallerRunsPolicy),但要做到心里有数,知道每种策略的副作用。

8. 异步调用的监控指标与可观测性设计

用了异步之后,问题排查难度确实会上升。同步模式下,一个请求从开始到结束是一条清晰的调用链,异步线程切来切去,传统日志就断了。所以异步用得越多,可观测性越要跟上。

至少要监控四个指标:

  1. 线程池活跃线程数
  2. 队列积压数量
  3. 任务执行耗时(P99分位)
  4. 被拒绝的任务数量

活跃线程数能看出线程池是否持续处于高水位;队列积压数量能提前预警任务堆积;执行耗时能看出单个任务的健康状况;被拒绝的任务数量直接反映系统是否已经超出容量。

日志配合MDC来做traceId传接,我在上一部分提过用TaskDecorator传上下文,就是为了让异步线程的日志也带上同一个traceId。每个异步任务开始和结束时各打一条日志,记录线程名、任务名、业务ID、耗时,自己画一条时间轴,基本能还原出任务执行的完整路径。

监控指标都接好之后,判断线程池参数是否合理的依据就变得具体了。线程池稳定在核心线程数附近、队列偶尔积压但能在几秒内排空,这是比较健康的状态。如果线程数长期贴近最大线程数、队列持续有积压,说明系统已经满负荷了,加线程不如削任务。

9. 异步改造的实际落地顺序

不管项目里选择哪种异步方案,都不建议一次性大规模改造。我见过的所有改造失败案例,几乎都是因为“把异步当成了万能的性能开关”,一口气把所有服务都标成@Async,结果线上问题此起彼伏,又不知道从哪里排起。

比较稳的推进方式是分四步走。

第一步,盘点场景。把项目里所有耗时超过1秒的操作都列出来,标注它们是否依赖主流程的结果。比如发送短信、生成日志、统计数据,这些明显是分支任务,适合异步化。

第二步,先做@Async打底。给这些分支任务加上线程池和异步注解,同时把异常处理器、日志、监控配套做好。这一步投入小,见效快,也是积累异步经验最快的方式。

第三步,局部引入WebFlux。当某个入口的并发瓶颈特别明显,比如聚合接口、网关入口,而这个服务的业务链路又相对清晰,这部分可以尝试换成响应式编程。不要一开始就把所有Service都改成Mono和Flux,链路过深会让改造成本成倍增长。

第四步,涉及跨系统的异步再上消息队列。消息队列引入的是整个中间件的运维成本,如果没有明确的削峰、解耦需求,就不要为了用而用。

10. 一点个人经验收尾

做异步调用这些年,最大的体会是:异步方案从来不是技术问题,而是取舍问题。它把当前线程的“等待时间”转移给了别的线程,换来的是接口响应速度的提升和系统吞吐量的改善,代价是代码复杂性、排查难度的上升,以及分布式场景下一致性问题的新增。

实际动手时,我的习惯是先画一张简单的调用链路图,标出每个环节的耗时、线程、依赖,再考虑在这个链路的哪个环节插入异步。不看链路就动手加注解,往往是在给系统埋雷。

如果你刚上手SpringBoot异步调用,不用急着把三套方案都吃透,先在自己项目里找一个实实在在的痛点,比如每次下发通知都阻塞请求、导出数据超时、日志写入拖慢业务,用@Async配合线程池改造一遍,把代理失效、上下文传递、异常监控这几个点亲手踩一遍,比看再多文档都有用。

工具选型也一样,没有绝对最好的方案。进程内的异步优先@Async,高并发聚合优先WebFlux,跨系统解耦优先消息队列。每个方案的代价不一样,选适合自己项目阶段的,已经足够。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进