@Async 咋总是不生效?!Spring Boot 异步编程 5 个“翻车”姿势,你中招了几个?

异步编程允许我们并发地执行任务,从而提升 API 的性能。从 Spring 3 开始,我们可以使用 @Async 注解来标记一个方法,使其可以被异步调用。

但是,有几个常见的错误 ❌ 可能会导致异步调用无法正确或高效地工作。


1. @Async 配置不当

为了让 @Async 注解正常工作,我们必须在配置类中添加 @EnableAsync 注解。

  • • 错误姿势:
    // 仅仅在 Service 方法上添加 @Async,但没有启用异步支持
    @Service
    public class UserService {
        @Async
        public void processUser(User user) {
            // 异步方法实现...
            System.out.println("处理用户 (异步): " + user.getName() + " on thread " + Thread.currentThread().getName());
        }
    }
    // 如果没有 @EnableAsync,上面的 @Async 将不会生效,方法会同步执行
  • • 正确姿势:
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import java.util.concurrent.Executor;
    
    @Configuration
    @EnableAsync// 1. 启用异步方法执行的支持
    publicclassAsyncConfiguration {
    
        @Bean(name = "asyncExecutor")// 2. 定义一个自定义的 Executor Bean
        public Executor asyncExecutor() {
            ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);        // 核心线程数
            executor.setMaxPoolSize(10);       // 最大线程数
            executor.setQueueCapacity(25);      // 队列容量
            executor.setThreadNamePrefix("TestAsyncThread-"); // 线程名前缀
            executor.initialize();
            return executor;
        }
        // 如果定义了自定义 Executor Bean,@Async 方法默认会使用它。
        // 也可以在 @Async("anotherExecutor") 中指定使用特定的 Executor。
    }
    
    // UserService 中的 @Async 方法现在会由 asyncExecutor 执行
    // @Service
    // public class UserService {
    //     @Async("asyncExecutor") // 可以显式指定
    //     public void processUser(User user) { ... }
    // }
  • • 注意事项:

    1. 1. 我们需要使用 @EnableAsync 来激活对异步调用的支持。

    2. 2. 我们应该使用一个自定义的 Executor (线程池) 来更有效地管理线程。默认情况下(即没有自定义 Executor Bean 时),@Async 注解会使用 Spring 的 SimpleAsyncTaskExecutor,它不是一个真正的线程池(每次调用都会创建一个新线程),在高并发下可能会导致创建过多线程,甚至引发 OutOfMemoryError (内存溢出错误)。


2. 异常处理不当以及忽略返回值

不对异步方法的返回值和可能抛出的异常进行妥善处理,可能会导致破坏性的代码和系统故障。

  • • 糟糕的代码:
    @Service
    publicclassReportService {
        @Async
        publicvoidgenerateReport() {
            // 这个方法如果发生异常,调用方无法感知,异常会静默失败
            System.out.println("开始生成报告 on thread " + Thread.currentThread().getName());
            if (true) { // 模拟异常
                thrownewRuntimeException("报告生成失败!");
            }
            System.out.println("报告生成成功 (理论上)");
        }
    
        publicvoidtriggerReportGeneration() {
            System.out.println("主线程发起报告生成...");
            generateReport(); // 调用异步方法,不关心其结果或异常
            System.out.println("主线程已发起,继续执行其他操作...");
        }
    }
  • • 改进后的代码:
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    import java.util.concurrent.CompletableFuture;
    
    @Service
    publicclassReportService {
        privatestaticfinalLoggerlogger= LoggerFactory.getLogger(ReportService.class);
    
        @Async("asyncExecutor")// 假设我们用了前面定义的 asyncExecutor
        public CompletableFuture<String> generateReport() {
            logger.info("异步开始生成报告...");
            try {
                // 模拟报告生成逻辑
                Thread.sleep(2000); // 模拟耗时操作
                if (Math.random() < 0.5) { // 模拟可能发生的失败
                    thrownewRuntimeException("模拟的报告生成内部错误!");
                }
                StringreportContent="报告内容:一切正常";
                logger.info("异步报告生成成功。");
                return CompletableFuture.completedFuture(reportContent); // 返回包含成功结果的 Future
            } catch (Exception e) {
                logger.error("异步报告生成失败。", e);
                return CompletableFuture.failedFuture(e); // 返回包含异常的 Future
            }
        }
    
        publicvoidprocessReport() {
            logger.info("主线程发起报告生成并处理结果...");
            CompletableFuture<String> reportFuture = generateReport();
    
            // 异步处理成功或失败的结果
            reportFuture.thenAccept(reportResult -> {
                logger.info("主线程收到异步报告结果: {}", reportResult);
                // 在这里处理成功生成的报告
            }).exceptionally(ex -> { // 处理异步方法中的异常
                logger.error("主线程捕获到异步报告生成失败的异常: {}", ex.getMessage());
                // 可以在这里执行备用逻辑,比如返回一个默认报告或错误提示
                return"生成报告时发生错误,请稍后再试。"; // exceptionally 需要一个返回值
            });
    
            logger.info("主线程已发起报告生成,可以继续做其他事情,结果将异步处理。");
        }
    }
  • • 要点:

    • • 为了有效地处理返回值和异常,我们可以让 @Async 方法返回 CompletableFuture<T>。我已经写过一篇关于 CompletableFuture 的文章 (原文提示,此处应有链接)。

    • • 对异常进行恰当的日志记录

    • • 调用方可以使用 CompletableFuture 的 thenAccept()thenApply()exceptionally()handle() 等方法来处理异步结果或异常。


3. 在异步方法中执行阻塞操作

有时,我们会在异步代码内部引入阻塞性逻辑,这完全违背了使用异步方法的初衷(即释放调用者线程,提高吞吐量)。如果异步线程池中的线程也因为阻塞操作而长时间被占用,那么异步处理的优势就荡然无存了。

  • • 阻塞的代码:
    @Service
    publicclassDataProcessingService {
        // @Autowired private DataRepository dataRepository; // 假设注入了
        // public void heavyProcessingMethod(Data item) { try{Thread.sleep(100);}catch(Exception e){} }
    
        @Async
        publicvoidprocessData() {
            logger.info("异步开始处理数据...");
            // 1. 阻塞式的数据库调用
            List<Data> dataList = dataRepository.findAll(); // 如果 findAll() 是阻塞的,这里会阻塞异步线程
    
            // 2. 同步的、阻塞式的循环处理
            for (Data item : dataList) {
                heavyProcessingMethod(item); // 如果这个方法是耗时的CPU密集型或阻塞I/O,同样会阻塞异步线程
            }
            logger.info("异步数据处理完成 (阻塞方式)。");
        }
    }
  • • 非阻塞的实现 (概念性示例):
    import java.util.stream.Collectors;
    // ... 其他 import
    
    @Service
    publicclassDataProcessingService {
        // @Autowired private DataRepository dataRepository; // 假设注入了
        // public Data processDataItem(Data item) { item.process(); return item;} // 假设 Data 有 process 方法
    
        @Async("asyncExecutor")
        public CompletableFuture<Void> processDataNonBlocking() {
            logger.info("异步开始非阻塞处理数据...");
            // 使用 CompletableFuture.supplyAsync 将整个操作提交到另一个线程池 (或者当前异步线程池)
            // 并且如果 dataRepository 支持响应式/异步查询,效果更佳
            return CompletableFuture.runAsync(() -> { // 或者 supplyAsync 如果需要返回结果
                List<Data> dataList = dataRepository.findAll(); // 理想情况下,这里应该是异步获取数据
    
                // 对于数据处理部分,可以考虑使用并行流 (parallelStream) 来加速CPU密集型任务
                // 注意:并行流使用的是 ForkJoinPool,不是我们配置的 asyncExecutor
                // 如果 processDataItem 是 I/O 密集型,可能需要更复杂的异步编排
                dataList.stream()
                    .parallel() // 尝试并行处理
                    .map(this::processDataItem) // 调用处理单个数据项的方法
                    .collect(Collectors.toList()); // 收集结果 (如果需要的话)
                logger.info("异步数据处理完成 (尝试非阻塞/并行方式)。");
            }, asyncExecutor); // 可以指定执行器
        }
    
        private Data processDataItem(Data item) {
            // 理想情况下,这里的处理也应该是异步/非阻塞的,
            // 或者至少是可以在并行流中安全高效执行的。
            logger.debug("处理数据项: {}", item.getId());
            return item.process(); // 假设 item.process() 是实际处理逻辑
        }
    }
    (要点:尽量避免在 @Async 方法中直接进行长时间的阻塞I/O。如果可能,将I/O操作也异步化,或者将其委托给专门的I/O线程池。对于CPU密集型任务,可以考虑在异步方法内部使用并行流等技术进一步优化。)

4. 事务管理使用不当

当 @Transactional 和 @Async 一起使用时,需要特别小心,因为它们可能不会像你期望的那样工作。默认情况下,事务上下文是与线程绑定的,异步方法在新线程中执行,可能不会继承调用方线程的事务上下文。

  • • 错误姿势 (可能导致事务问题):
    @Service
    publicclassUserService {
        // @Autowired private UserRepository userRepository;
    
        @Transactional// 事务注解
        @Async         // 异步注解,这两个注解直接用在同一个方法上时要特别小心
        publicvoidupdateUserStatus(Long userId) {
            // 事务可能不会按预期工作。
            // 如果 @Async 的代理先生效,则 @Transactional 可能在错误的线程或时机作用,甚至无效。
            // 如果 @Transactional 的代理先生效,那么异步执行的部分可能在事务提交后才开始,或者在新线程中没有事务。
            logger.info("异步尝试更新用户 {} 状态 (可能有事务问题)", userId);
            Useruser= userRepository.findById(userId).orElseThrow(() -> newRuntimeException("User not found"));
            user.setStatus(UserStatus.ACTIVE);
            userRepository.save(user);
            logger.info("用户 {} 状态已更新 (可能有事务问题)", userId);
        }
    }
  • • 正确的代码 (将事务与异步分离):
    import org.springframework.transaction.annotation.Propagation;
    // ... 其他 import
    
    @Service
    publicclassUserService {
        // @Autowired private UserRepository userRepository;
        // @Autowired private UserService self; // 用于自调用代理 (或另一个Service)
    
        // 同步的、事务性的入口方法
        @Transactional// 这个方法在调用者线程中,在一个事务内执行
        publicvoidinitiateUserStatusUpdate(Long userId) {
            logger.info("发起用户 {} 状态更新流程 (事务性)", userId);
            // 从这里调用异步方法。通常需要通过代理调用才能使 @Async 生效。
            // 如果 updateUserStatusAsync 在同一个类中,直接调用 this.updateUserStatusAsync()
            // 可能不会触发 @Async 的代理。推荐将其放在另一个 Service 类中,或者注入自身代理。
            // self.updateUserStatusAsync(userId);
            updateUserStatusAsync(userId); // 简化示例,假设代理能正确工作或在不同类中
        }
    
        // 异步方法,可以有自己的事务配置
        @Async("asyncExecutor")
        @Transactional(propagation = Propagation.REQUIRES_NEW)// 异步方法通常需要开启一个新的事务
        publicvoidupdateUserStatusAsync(Long userId) {
            logger.info("异步开始更新用户 {} 状态 (新事务)", userId);
            Useruser= userRepository.findById(userId).orElseThrow(() -> newRuntimeException("User not found"));
            user.setStatus(UserStatus.ACTIVE);
            userRepository.save(user);
            logger.info("用户 {} 状态已异步更新 (新事务)", userId);
        }
    }
    (要点:通常,最佳实践是将事务边界控制在同步方法中,然后由该同步方法去调用异步方法。异步方法如果也需要事务,应该配置为 Propagation.REQUIRES_NEW 来开启一个独立的、新的事务。)

5. 从同一个类中调用异步方法

@Async 注解的异步功能是通过 Spring AOP 代理机制来实现的。这意味着,如果你在同一个类的一个普通方法中,直接通过 this 关键字调用该类中另一个标记了 @Async 的方法,那么异步效果将会失效(因为调用没有经过代理对象)。

  • • 错误姿势:
    @Service
    publicclassTestService {
        // @Autowired private AsyncService asyncService; // 应该注入另一个服务
    
        publicvoidsomeSyncMethodThenAsync() {
            logger.info("同步方法开始...");
            // 一些逻辑...
            this.internalAsyncMethod(); // 错误!通过 this 调用,@Async 不会生效,会变成同步调用
            logger.info("同步方法结束 (可能在 internalAsyncMethod 执行完后才结束)。");
        }
    
        @Async("asyncExecutor")
        publicvoidinternalAsyncMethod() {
            logger.info("internalAsyncMethod 正在执行 on thread " + Thread.currentThread().getName());
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            logger.info("internalAsyncMethod 执行完毕。");
        }
    }
  • • 正确的实现 (将异步方法移到另一个 Bean 中):
    // 主服务类
    @Service
    publicclassTestService {
    
        @Autowired
        private AsyncHelperService asyncHelperService; // 注入包含异步方法的辅助 Bean
    
        publicvoidtriggerAsyncCall() {
            logger.info("TestService: 发起异步调用...");
            asyncHelperService.performAsyncTask(); // 通过代理调用,@Async 生效
            logger.info("TestService: 异步调用已发起,主流程继续...");
        }
    }
    
    // 包含异步方法的辅助 Service 类
    @Service
    publicclassAsyncHelperService {
    
        @Async("asyncExecutor")
        publicvoidperformAsyncTask() {
            logger.info("AsyncHelperService: performAsyncTask 正在执行 on thread " + Thread.currentThread().getName());
            try {
                Thread.sleep(2000); // 模拟耗时操作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            logger.info("AsyncHelperService: performAsyncTask 执行完毕。");
        }
    }
    (要点:要确保 @Async 生效,调用异步方法的请求必须通过 Spring 创建的代理对象。最简单的方法是将异步方法放在一个单独的 Bean 中,然后通过依赖注入来调用它。)

以上就是一些我们在使用 @Async 注解时常犯的错误。还有其他一些情况也容易被忽略,比如异步方法的调用顺序不当,这可能导致结果的随机性或不确定性,需要根据具体业务场景仔细设计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值