当前位置: 首页 > news >正文

踩坑了!Spring Boot 异步调用 CompletableFuture 的坑你踩过几个

踩坑了!Spring Boot 异步调用 CompletableFuture 的坑你踩过几个

说实话,我第一次用 CompletableFuture 的时候,觉得这玩意儿真香异步并行、线程池管理,比原来的 Future 好用太多了。结果上线后,BUG 一个接一个,内存飙升、线程阻塞、异常丢失…踩了整整两天的坑,才把问题全部排查清楚。今天就把这些血泪教训分享出来,希望能帮大家少走弯路。

坑一:默认线程池 - 异步调用变成同步阻塞

我一开始这么写:

@ServicepublicclassOrderService{publicOrderDTOgetOrderDetail(LongorderId){// 异步查询用户信息和订单信息CompletableFuture<UserDTO>userFuture=CompletableFuture.supplyAsync(()->userMapper.findById(orderId));CompletableFuture<OrderDTO>orderFuture=CompletableFuture.supplyAsync(()->orderMapper.findById(orderId));// 等待结果OrderDTOorder=orderFuture.join();UserDTOuser=userFuture.join();// 组装返回order.setUser(user);returnorder;}}

看起来没问题,两个查询并行执行,性能应该不错。结果压测的时候,接口响应时间越来越长,最后直接超时。

原因分析

CompletableFuture.supplyAsync()默认使用ForkJoinPool.commonPool(),这个线程池的线程数是CPU 核心数 - 1。在 Spring Boot 应用中,很多组件(Tomcat、数据库连接池等)都会抢占这个线程池,一旦线程池耗尽,异步任务就会排队等待,变成实际上的同步执行。

解决方案

自定义线程池,根据业务场景配置合理的线程数:

@ConfigurationpublicclassThreadPoolConfig{@Bean("asyncExecutor")publicExecutorasyncExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();// 核心线程数executor.setCorePoolSize(10);// 最大线程数executor.setMaxPoolSize(20);// 队列容量executor.setQueueCapacity(100);// 线程名前缀executor.setThreadNamePrefix("async-");// 拒绝策略executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();returnexecutor;}}

使用时指定线程池:

CompletableFuture<UserDTO>userFuture=CompletableFuture.supplyAsync(()->userMapper.findById(orderId),asyncExecutor);

最佳实践:为不同类型的异步任务配置不同的线程池,避免相互影响。比如 IO 密集型任务可以设置更大的线程数,CPU 密集型任务则需要控制线程数。

坑二:异常丢失 - 异步任务抛异常却没人管

遇到这种情况:

@ServicepublicclassPaymentService{publicvoidprocessPayment(OrderDTOorder){CompletableFuture.runAsync(()->{// 调用第三方支付接口paymentGateway.pay(order);// 更新订单状态orderMapper.updateStatus(order.getId(),"PAID");});// 直接返回成功log.info("支付处理已提交");}}

问题是:如果第三方支付接口抛出异常,orderMapper.updateStatus()不会执行,订单状态永远是"待支付",但用户却收到了"成功"的提示。钱没了,状态没改,这可就是严重的事故了。

原因分析

CompletableFuture.runAsync()没有返回值,默认也不会处理异常。即使加了exceptionally(),如果忘记加,异常就会被吞掉,悄无声息。

解决方案

一定要处理异常,至少要记录日志:

CompletableFuture.runAsync(()->{paymentGateway.pay(order);orderMapper.updateStatus(order.getId(),"PAID");},asyncExecutor).exceptionally(ex->{// 记录异常日志log.error("支付处理失败,订单ID: {}, 异常: {}",order.getId(),ex.getMessage(),ex);// 发送告警通知alertService.sendAlert("支付异常",order.getId(),ex);// 更新订单状态为失败orderMapper.updateStatus(order.getId(),"PAY_FAILED");returnnull;});

或者使用handle()方法,可以处理正常结果和异常:

CompletableFuture.runAsync(()->{paymentGateway.pay(order);orderMapper.updateStatus(order.getId(),"PAID");},asyncExecutor).handle((result,ex)->{if(ex!=null){log.error("支付处理失败",ex);orderMapper.updateStatus(order.getId(),"PAY_FAILED");}returnresult;});

重要提醒:如果是核心业务流程,异步任务失败后一定要有补偿机制,比如重试、人工介入、告警通知等。

坑三:线程上下文丢失 - @Async 和 ThreadLocal 的坑

遇到这种问题:

@ServicepublicclassUserService{privatestaticfinalThreadLocal<String>TRACE_ID=newThreadLocal<>();publicvoidcreateUser(UserDTOuser){TRACE_ID.set(generateTraceId());CompletableFuture.runAsync(()->{// 在异步线程中获取 traceIdStringtraceId=TRACE_ID.get();// null!log.info("创建用户,traceId: {}",traceId);userMapper.insert(user);});}}

在主线程设置的TRACE_ID,在异步线程中获取不到,导致日志追踪链路中断,排查问题的时候根本找不到完整的调用链。

原因分析

CompletableFuture.supplyAsync()会使用新的线程去执行任务,新线程和主线程不是同一个线程,ThreadLocal的数据无法共享。

解决方案

使用阿里巴巴开源的TransmittableThreadLocal

importcom.alibaba.ttl.TransmittableThreadLocal;importcom.alibaba.ttl.TtlRunnable;@ServicepublicclassUserService{privatestaticfinalTransmittableThreadLocal<String>TRACE_ID=newTransmittableThreadLocal<>();publicvoidcreateUser(UserDTOuser){TRACE_ID.set(generateTraceId());// 使用 TtlRunnable 包装Runnabletask=TtlRunnable.get(()->{StringtraceId=TRACE_ID.get();// 可以获取到!log.info("创建用户,traceId: {}",traceId);userMapper.insert(user);});CompletableFuture.runAsync(task,asyncExecutor);}}

如果使用 Spring 的@Async注解,TTL 也提供了自动支持的代理:

@ConfigurationpublicclassAsyncConfigimplementsAsyncConfigurer{@OverridepublicExecutorgetAsyncExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(100);executor.setThreadNamePrefix("async-");executor.initialize();// 使用 TtlExecutorWrapper 包装returnTtlExecutorWrapper.get(executor);}}

这样无论是手动创建的CompletableFuture还是@Async注解,ThreadLocal 都能正常传递。

坑四:CompletableFuture 组合调用 - allOf 导致的资源泄露

这种写法有潜在问题:

publicvoidprocessBatchOrders(List<Long>orderIds){List<CompletableFuture<Void>>futures=orderIds.stream().map(id->CompletableFuture.runAsync(()->{processOrder(id);},asyncExecutor)).collect(Collectors.toList());// 等待所有任务完成CompletableFuture.allOf(futures.toArray(newCompletableFuture[0])).join();log.info("批量处理完成");}

在高频调用场景下,如果processOrder()抛出异常,allOf.join()会抛出异常。但关键是:如果某些任务已经创建但还没开始执行,线程池中的任务可能会被取消,导致资源未正确释放。

原因分析

CompletableFuture.allOf()的行为是:如果任意一个任务抛出异常,allOf会立即抛出异常,但其他正在执行的任务会继续运行(不会取消)。如果任务中有占用资源(数据库连接、文件句柄、网络连接等),可能会导致资源泄露。

解决方案

  1. 使用orTimeout()防止无限等待
CompletableFuture.allOf(futures.toArray(newCompletableFuture[0])).orTimeout(30,TimeUnit.SECONDS).join();
  1. 使用whenComplete确保资源释放
List<CompletableFuture<Void>>futures=orderIds.stream().map(id->CompletableFuture.runAsync(()->{try{processOrder(id);}finally{// 确保资源释放releaseResources(id);}},asyncExecutor)).collect(Collectors.toList());CompletableFuture.allOf(futures.toArray(newCompletableFuture[0])).whenComplete((result,ex)->{if(ex!=null){log.error("批量处理出现异常",ex);}}).join();
  1. 使用信号量控制并发数
Semaphoresemaphore=newSemaphore(10);// 最多10个并发List<CompletableFuture<Void>>futures=orderIds.stream().map(id->CompletableFuture.runAsync(()->{try{semaphore.acquire();processOrder(id);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{semaphore.release();}},asyncExecutor)).collect(Collectors.toList());

写在最后

CompletableFuture 是 Java 异步编程的利器,但用不好就是坑。建议大家在项目中:

  1. 自定义线程池,根据业务场景配置参数,不要使用默认线程池
  2. 异常处理是必须的,至少记录日志,核心业务流程要有补偿机制
  3. ThreadLocal 传递使用TransmittableThreadLocal,避免链路中断
  4. 资源控制使用超时、信号量等机制,防止资源泄露

异步编程看起来美好,但细节决定成败。希望这篇文章能帮大家避坑,如果觉得有帮助,点个赞再走~


你踩过 CompletableFuture 的哪些坑?评论区聊聊,互相避坑!

http://www.jsqmd.com/news/385032/

相关文章:

  • 公路隧道铁路地铁隧道漏水隧道渗水识别分割数据集labelme格式2758张1类别
  • 桐庐中职舞蹈表演专业有哪些?最新解析,表演系艺术职高学校/艺术类职高/化妆职高/化妆专业中职/艺体职高,中职厂家有哪些 - 品牌推荐师
  • 轻量化模型浪潮下的关键技术突破:DeepSeek INT4量化优化引领2026端侧算力新纪元
  • Linux apt 命令
  • 2026年2月西南青少年儿童视力验光中心推荐,专业设备与配镜保障优选 - 品牌鉴赏师
  • Percy深度解析
  • Linux yum 命令
  • 揭秘关键!AI应用架构师揭秘企业算力资源调度关键
  • 北京宠物训练基地哪家好?北京宠物训练基地top榜单(2026年新版) - 品牌2025
  • STM32H750串口DMA收发实验源码 采用串口空闲中断接收,处理不定长数据, dma直接发...
  • 商贸加工行业数字化管理系统设计与实现(Python)
  • Visual Regression Testing深度解析
  • Navicat Premium 17 专业版安装及使用教程
  • 小白/程序员入门大模型:AI产品经理的职责与必备技能解析,大模型产品经理需要哪些必备技能?
  • 【Linux网络】基于Reactor反应堆模式的高并发服务器深度解析:原理、实战与踩坑记录
  • 张建国2026到2028信奥课程学习规划书
  • 大模型时代的产品经理:为何必须学习,零基础小白也能学会的大模型,产品经理必备技能!
  • 巴菲特的投资方法与长期收益策略
  • 15分钟发布两大AI模型,万亿美元蒸发!DeepSeek V4将至,小白程序员该收藏这篇看懂趋势!
  • 小白程序员必看:分块决定RAG质量,掌握它才能用好大模型!
  • Jasmine + Karma深度解析
  • C端产品经理转型大模型:收藏这份学习路线,小白也能轻松入门!
  • php 6
  • Dify企业级架构:小白也能学会的大模型部署与收藏指南
  • DeepSeek 悄然升级:百万级上下文窗口开启,国产大模型收藏必备!
  • 智能资源调度AI引擎,助力AI应用架构师实现技术突破
  • 一天一个开源项目(第23篇):PageLM - 开源 AI 教育平台,把学习材料变成互动资源
  • 北京宠物寄养学校哪家条件和服务比较好?北京宠物寄养宾馆酒店推荐 - 品牌2025
  • 人机协同开发:效率提升的新模式
  • Test Utils + Vitest深度解析