恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Spring Boot异步与同步任务实战:从@Async到线程池,彻底告别接口卡顿

  • 首页
  • 资讯中心
  • /
  • Spring Boot异步与同步任务实战:从@Async到线程池,彻底告别接口卡顿

相关资讯

共轭梯度法全面解析:原理推导、代码实现与预条件实践 2026/10/16 9:57:53
Flutter网络拦截器在鸿蒙端的适配实践:全量拦截、动态Header注入与流量审计 2026/10/16 9:57:53
Seaborn实战:用Titanic数据集掌握数据可视化核心技巧 2026/10/16 9:52:52

最新资讯

MATLAB实现DAS波束形成:从时延计算到超声成像实战解析
Redis性能优化实战:从大Key、热Key到持久化与连接池的完整排查指南
陕西GogoAI自助健身小程序分销营销模块搭建
陕西GogoAI自助健身小程序核销功能开发解析
西安GogoAI健身系统开发流程,需求调研到上线部署
贡献法与单调栈:O(n)求解所有子数组最小值之和

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Spring Boot异步与同步任务实战:从@Async到线程池,彻底告别接口卡顿

发布时间:2026/10/16 9:57:53
Spring Boot异步与同步任务实战:从@Async到线程池,彻底告别接口卡顿 最近接手了一个老项目线上时不时出现功能卡顿点一个按钮要等十几秒才响应查了半天发现是用户注册后要同步发送邮件和短信还要调两个外部系统接口全挤在请求线程里。这个案例很典型地说明了SpringBoot开发中同步和异步任务如果分不清迟早会出问题。今天就拿这个场景聊聊SpringBoot里的异步与同步任务到底怎么选、怎么写、怎么避免踩坑。1. 先搞清楚什么任务需要异步什么时候必须同步很多同学一听说异步可以提高性能就恨不得把所有方法都加上Async结果线上问题一大堆。其实异步不是银弹它本质上是“把线程让出来让请求先返回后台慢慢干”代价是失去了“实时结果”和“强一致性”。1.1 同步任务的真实面孔同步任务是SpringBoot默认的行为一个请求进来分配一个Tomcat线程从Controller到Service到Mapper一路顺序执行最后把结果返回给客户端。这个过程里线程是占用着的直到方法结束。如果某个操作耗时5秒客户端就要等5秒Tomcat线程也一直被占着。比如用户注册场景校验参数、加密密码、插入用户表必须同步因为后续流程依赖这个结果。返回用户ID给前端也必须同步因为前端要立刻拿到。这些操作如果改成异步就会出现“注册还没完成接口就返回成功”的错觉用户看到成功提示但实际账号不存在这显然不行。1.2 异步任务解决的核心痛点异步解决的是“不影响主流程但必须保证最终完成”的场景。最经典的就是通知类操作发邮件发短信推送站内信写操作日志调用第三方回调接口这些操作的共性是用户不关心你是否在100毫秒内把邮件发出去但你又必须发。如果把它们同步做每封邮件假设耗时200毫秒注册接口就要额外增加几百毫秒甚至几秒的等待。改成异步后接口立刻返回邮件在后台线程慢慢发。用生活里的例子说同步就是你去银行柜台办业务柜员全程跟你一对一办完一个才叫下一个异步就是你先抽个号叫号之前你可以在大厅玩手机柜台后面多个人同时处理业务。1.3 判断同步还是异步的三个标准我总结了一套判断标准帮你快速决策判断维度选择同步选择异步是否需要立即知道结果是比如登录查询否比如发通知主流程是否依赖该操作的成功是比如扣库存否比如写日志失败后是否需要回滚主流程是比如支付否比如短信发送失败不影响注册如果三个回答都是“是”老老实实同步如果“不需要立即知道结果”、“不依赖”、“失败不影响”就值得考虑异步。2. SpringBoot实现异步任务的完整步骤SpringBoot对异步的支持非常成熟基于Spring的Async注解加一个启动开关几分钟就能跑起来。但“能跑”和“跑得稳”中间差着一大堆细节。2.1 三步开启异步功能第一步在启动类或配置类上加EnableAsyncSpringBootApplication EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }第二步在需要异步执行的方法上打Async注解Service public class NoticeService { Async public void sendMail(Long userId, String content) { // 真实项目里这里是调用邮件服务 Thread.sleep(300); System.out.println(发送邮件给用户 userId); } }第三步通过注入的方式调用这个方法而不是直接newService public class RegisterService { private final NoticeService noticeService; public RegisterService(NoticeService noticeService) { this.noticeService noticeService; } public void register(User user) { // 1. 同步校验、插入用户表 userMapper.insert(user); // 2. 异步发送邮件 noticeService.sendMail(user.getId(), 欢迎注册); // 3. 接口直接返回 } }这里必须强调Async是通过Spring AOP代理实现的。只有从其他Bean注入调用时请求才会经过代理对象才会进入异步逻辑。2.2 同类内部调用导致异步失效这是新手最容易踩的坑。看这个例子Service public class NoticeService { Async public void sendMail(Long userId) { // ... } public void process() { // 直接调用 this异步失效变成了同步执行 this.sendMail(1001L); } }this.sendMail()调用的是原始对象的方法AOP代理根本没机会介入Async自然不生效。解决办法有两种第一种把需要异步的方法拆到另一个Bean里或者注入自己。用ApplicationContext重新拿代理对象Service public class NoticeService { Autowired private ApplicationContext applicationContext; public void process() { applicationContext.getBean(NoticeService.class).sendMail(1001L); } }第二种开启AOP暴露代理然后使用AopContext.currentProxy()spring.aop.proxy-target-classtrue spring.aop.autofalse # 如果报错可以设置 exposeProxytrue 场景不同我个人的建议是不要在同一个类里写“我为我自己提供异步方法”的设计拆一个独立Service更清爽测试也方便。2.3 异步方法返回值的正确姿势Async方法可以返回void也可以返回Future或CompletableFuture。如果你需要拿到异步结果直接返回CompletableFuture最灵活Async public CompletableFutureOrderStatus queryExternalOrder(Order order) { // 模拟调用外部系统 OrderStatus status externalClient.query(order.getId()); return CompletableFuture.completedFuture(status); }调用方可以组合多个异步结果CompletableFutureOrderStatus f1 orderService.queryExternalOrder(order1); CompletableFutureOrderStatus f2 orderService.queryExternalOrder(order2); CompletableFutureVoid all CompletableFuture.allOf(f1, f2); all.join(); // 等待全部完成 OrderStatus status1 f1.get();注意当方法返回CompletableFuture时Spring会使用内置的AsyncTaskExecutor提交任务任务返回结果也会封装到Future里。如果方法异常调用方get()时会抛出ExecutionException。2.4 异步方法访问范围必须publicAsync生效的条件之一就是方法必须是public因为Spring AOP只能代理public方法。如果你把Async放在private方法上编译虽然不报错但运行时期望落空没有任何提示排查起来非常隐蔽。这一点不写在官方文档的前几行但生产环境很容易遇到。3. 异步线程池别让默认配置背锅很多项目加了个Async就上线了结果高峰期申请线程数瞬间爆炸。原因就是默认的异步执行器是SimpleAsyncTaskExecutor这个家伙每次执行任务都会new一个线程执行完就丢弃没有复用没有上限线上高并发时几万个线程都有可能。3.1 线程池参数从哪里来正确的做法是自定义ThreadPoolTaskExecutor并设置核心线程数、最大线程数、队列容量和拒绝策略。先给出一份可以直接抄的配置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.concurrent.ThreadPoolTaskExecutor; import java.util.Arrays; import java.util.concurrent.ThreadPoolExecutor; Configuration public class AsyncConfig implements AsyncConfigurer { Bean(taskExecutor) public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); // 核心线程数常驻线程 executor.setCorePoolSize(8); // 最大线程数队列满后最多扩充到多少 executor.setMaxPoolSize(16); // 队列容量缓冲突发任务 executor.setQueueCapacity(100); // 线程名前缀 executor.setThreadNamePrefix(async-task-); // 拒绝策略这里用 CallerRunsPolicy executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 优雅停机等待已提交的任务完成 executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; } Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, params) - { System.err.println(异步方法异常: method.getName() , 参数: Arrays.toString(params)); throwable.printStackTrace(); }; } }这里有几个参数要好好解释一下不然你也不知道配多少。3.2 核心线程数怎么算线程池参数不能拍脑袋要结合任务类型。如果是CPU密集型任务也就是计算不吃IO核心线程数可以设为CPU核数 1。如果是IO密集型任务也就是大量时间在等数据库、等接口、等网络核心线程数可以设大一些常见估算公式是核心线程数 CPU核数 / (1 - 阻塞系数)阻塞系数通常在0.8到0.9之间。假设服务器4核阻塞系数0.8那么核心线程数就是4 / 0.2 20。但要注意如果任务里有大量数据库操作线程数不是越大越好因为数据库连接池本身就是瓶颈。你开20个线程每个都要拿连接连接池才10个剩下10个线程就卡在等待连接上。所以我通常建议异步任务的并发数上限不要超过下游资源数据库连接池、外部接口限流能承受的上限。比如数据库连接池最大20核心线程就设在8到10剩下用队列缓冲。3.3 队列容量与拒绝策略的搭配线程池的执行流程很多人搞反了。ThreadPoolTaskExecutor默认是这样的核心线程没满直接新建线程执行任务。核心线程满了任务进队列。队列满了才创建新线程执行任务直到最大线程数。最大线程也满了触发拒绝策略。所以队列容量关系到“积压能力”。队列太小突发流量一来就会触发拒绝策略队列太大大量任务排队时效性差内存压力也大。一般经验是对于发通知、写日志这类允许延迟的任务队列可以设100到200。对于需要快速响应的任务队列设小一点宁可拒绝掉也要避免积压。拒绝策略常见有四种AbortPolicy直接抛异常默认策略。CallerRunsPolicy谁提交的任务谁自己执行也就是说调用线程自己来做。这不会丢任务但会影响同步接口的响应时间。DiscardPolicy直接丢弃新任务。DiscardOldestPolicy丢弃队列里最旧的任务。具体选哪个要看业务容忍度。如果异步发送通知失败是可以容忍的我建议选CallerRunsPolicy它至少保证任务不会丢代价是在极端情况下异步变成同步反压在请求线程上。如果担心拖垮接口可以换成AbortPolicy配合告警。3.4 多个线程池如何指定一个项目里往往有多个异步场景比如一个负责邮件通知一个负责日志采集一个负责外部接口数据同步。如果都用同一个线程池一个队列堆满会连累另一个业务。推荐的做法是定义多个Executor bean然后在Async注解里指定线程池名称Async(mailExecutor) public void sendMail(Long userId) { ... } Async(logExecutor) public void writeLog(String log) { ... }如果不指定名称Spring使用Async注解方法所在的Bean容器里唯一Executor如果有多个Executor但不指定会报错找不到了taskExecutor。4. 同步任务和异步任务混用时的常见坑把异步引入一个原本全是同步的项目不只是加个注解那么简单。我见过太多项目因为异步混用把事务搞丢、把上下文搞丢、把停机搞挂的。这里逐个说。4.1 Async和Transactional在一起的“假事务”先看这个代码Async Transactional public void updateUserCredit(Long userId, Integer amount) { creditMapper.add(userId, amount); messageMapper.insert(...); }看起来没什么问题但实际运行中事务很可能没有开启。原因是Spring的事务管理也是靠AOP代理实现的而Async会让方法在线程池的新线程里执行代理关系就变得复杂。更关键的是声明式事务底层通过ThreadLocal把数据库连接绑定到当前线程异步线程拿不到主线程里的连接自然无法组成一个事务。如果你坚持在异步方法里做数据库更新我建议使用编程式事务手动控制Async public void updateUserCredit(Long userId, Integer amount) { transactionTemplate.execute(status - { creditMapper.add(userId, amount); messageMapper.insert(...); return null; }); }但更大的最佳实践是异步方法里不要做强一致性的写操作。异步做的一定是“失败可容忍、可补偿、可重试”的事。比如发邮件失败就失败最多记个错误日志但扣钱、减库存这种操作绝对不能异步化否则分布式事务会把你折磨到崩溃。4.2 异步任务的异常很容易被吞掉同步方法抛异常调用方能立刻感知事务也可以回滚。但void的异步方法抛异常时调用方已经返回了异常只能交给AsyncUncaughtExceptionHandler处理。如果你没有配置这个Handler异常会直接打印到控制台线上日志很难发现。所以务必在AsyncConfigurer里实现getAsyncUncaughtExceptionHandler()把异常记录下来并接入告警。我在前面给出的配置里已经写了你可以把System.err.println替换成自己的日志组件log.error(异步任务执行异常, method{}, params{}, method.getName(), Arrays.toString(params), throwable);如果你用的异步方法返回CompletableFuture异常会在调用get()时抛出但也别忘了在回调里处理异常分支。4.3 请求上下文在子线程里丢失这个坑遇到一次就会长记性。某个项目在异步方法里想通过RequestContextHolder拿当前请求的用户ID结果运行时报RequestAttributes is null。别问为什么子线程根本没有ServletRequest的ThreadLocal信息。解决思路分两种第一种最简单的在进入异步方法之前把需要的参数显式传递进去Async public void sendMail(Long userId, String email, String content) { // 直接用参数 }第二种使用TaskDecorator包装Runnable把主线程的上下文比如日志追踪ID、用户信息传递到子线程。这个方案对代码侵入小如果你使用了日志MDC、租户ID、SecurityContext等机制非常推荐。public class ContextCopyingDecorator implements TaskDecorator { Override public Runnable decorate(Runnable runnable) { MapString, String contextMap MDC.getCopyOfContextMap(); return () - { try { if (contextMap ! null) { MDC.setContextMap(contextMap); } runnable.run(); } finally { MDC.clear(); } }; } }然后在Executor上设置executor.setTaskDecorator(new ContextCopyingDecorator());注意SecurityContextHolder默认模式是ThreadLocal要跨线程传递需要把模式改成MODE_INHERITABLE_THREADLOCAL或者在TaskDecorator里手动复制。这个细节很多项目都会漏。4.4 应用停机时异步任务丢得悄无声息开发环境看不出问题一上生产发版本偶尔出现“数据没发出去”的投诉。最常见原因就是线程池没有优雅停机。当应用开始关闭时Spring容器会销毁Bean但如果线程池里的任务还在执行或者队列里还有任务没执行就直接丢了。配置优雅停机就是两行关键代码executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60);第一个参数表示关闭线程池时等待所有已提交任务完成。第二个参数表示最多等60秒超过就强制关闭。这两个参数配合SpringBoot的spring.lifecycle.timeout-per-shutdown-phase可以解决大部分停机丢任务的问题。5. 一次异步故障排查实录与常见问题速查前面讲了原理和配置最后分享一个真实的排查案例。这是我们团队某次上线后遇到的典型问题花了一个多小时才定位到根因。5.1 现象数据库连接池被率先打满某公司上线了一个积分商城活动用户签到后系统自动发积分。上线半小时线上开始频繁报错HikariPool-1 - Connection is not available, request timed out after 30000ms.数据库连接池最大设置的是20同步接口本身没多少压力但连接池就是被打满了。一开始以为是SQL慢查询查了一圈发现SQL全是主键查询单次不到1毫秒。后面看了线程dump发现一个触目惊心的事实应用里有几千个名为SimpleAsyncTaskExecutor-xxx的线程。原来签到接口里调用了Async的积分发放方法但没配置自定义线程池默认用的SimpleAsyncTaskExecutor。这个执行器每来一个任务就新建一个线程瞬间几百个线程同时去数据库拿连接直接把连接池榨干了。5.2 解决线程池隔离与参数落位我们把异步任务拆成了独立的taskExecutor核心线程6最大线程12队列100拒绝策略用CallerRunsPolicy并给积分发放设置了单独的名称前缀。上线后线程数量稳定在10个左右数据库连接池也不再超时。同时还做了另一件事把同步请求和异步任务用的连接池做了隔离给异步任务专用一个数据库连接池最大5个连接。这样即使异步任务再如何失控也不会把同步接口的数据库连接池占光。所谓“线程池隔离”不是只隔离线程连数据库资源也要一起隔离。5.3 常见问题速查表现象可能原因解决方案Async方法没有异步执行同类内部调用、没加EnableAsync、非public方法拆独立Service、加注解、改public异步任务无限创建线程使用了SimpleAsyncTaskExecutor配置ThreadPoolTaskExecutor异步方法里事务不生效声明式事务基于ThreadLocal异步线程不属于原事务用编程式事务或异步里不写强一致数据请求上下文或用户信息丢失子线程拿不到主线程ThreadLocal参数显式传递或用TaskDecorator停机时任务丢失线程池未等任务完成就直接关闭开启WaitForTasksToCompleteOnShutdown异步异常无日志无告警没有配置AsyncUncaughtExceptionHandler实现AsyncConfigurer统一处理一个线程池堆满影响别的异步业务多个业务共用一个线程池按业务拆多个Executor用Async(name)指定5.4 关于异步与同步最后分享几条体会我个人把这一整套踩坑经历浓缩成几条规则放在心里基本能避开绝大多数坑第一条能用同步想清楚再异步。不要为了“技术感”去异步。同步是默认的、稳妥的、可追踪的。只有当某项操作明显拖慢主流程而且你愿意承担“最终一致”时才考虑异步。第二条异步一定要有独立的线程池并且按业务拆分。线上没有“一个线程池走天下”这回事拆分不仅能保护同步接口还能让不同任务互不干扰。第三条异步方法默认当作不可靠的。它可能丢失、可能延迟、可能部分失败所以在代码层面要设计告警和补偿机制至少要能记录失败现场否则出了问题你连从哪查都不知道。如果你现在正打算在SpringBoot项目里用异步不妨先把EnableAsync注释掉先在纸上画清楚哪些是同步主链路哪些是异步补偿链路再动手写代码。这样通常能省下后来一大笔排查时间。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号