Springboot @Async 失效的坑

本文介绍了一种在SpringBoot环境下通过异步处理提高接口响应速度的方法。针对工单批量转移时前端调用超时的问题,文章详细阐述了如何利用@Async注解进行异步处理,并分享了实施过程中遇到的问题及解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异步应用场景

为了提高接口的响应性能,当业务非常复杂的情况下,可以将一部分跟业务关联性不是特别强的逻辑进行异步处理。如日志记录、短信发送、增加积分等。通常而言会将此类业务逻辑通过异步的方式进行处理,从而加快接口的响应速度,常用的解决方案有:

  • 使用JDK 自定义线程池 让代码异步执行
  • 在springboot 中 使用@Async注解进行异步处理
  • 使用中间件如mq 消息通知让下游异步消费 如RocketMQ、KAFKA

使用第一种方式,需要精通线程池运行原理,结合实际的业务场景对队列大小进行合理的设置。队列设置过大过小都会存在内存溢出的风险。

第三种方式是最合理的方式,它能够通过MQ进行削峰填谷,通过合理的参数配置,保证数据不会丢失。但是架构改动过大,对小型的单体应用来讲,工作量过大,成本过高。

在springboot 大行其道的情况下,考虑开发成本,以及项目时间关系选用第二种方式来解决代码异步执行的问题。

真实业务场景

线上问题

一个工单的分页列表,前端控制了每个列表最大的显示条数为100条。在业务流程中存在工单转移的操作,转移一笔工单至少包含以下几个重要的步骤:

  1. 新增工单处理日志,如什么时间点将工单转移给某人
  2. 修改工单当前处理人
  3. 发送企业微信给B端的跟进人(转移人)
  4. 发送im信息给C端的用户

由于公司采用微服务架构,因此每个业务模块拆分的很细,在上述步骤中需要从其他系统中通过rpc调用接口拿到需要的数据才能完成整个业务流程数据的拼装,如需要从crm系统拿到组织架构信息,获取转移人的组织架构、需要从udb用户中心获取转移人的企业微信昵称等。

因此在批量转移的时候,前端会出现调用超时的问题,原因是dubbo接口默认的超时时间是15秒,由于业务复杂,导致在15秒内执行不完业务逻辑。

解决方案

  1. 将sql处理改为批量执行,如新增处理日志 (batch insert);修改当前工单,使用case when 的方式一次性修改完成(批量update)
  2. 将发送消息改成异步处理 加快前端接口的响应速度
  3. 让接口提供方提供批量查询的接口,避免rpc 循环调用在网络上的消耗

优化完成之后,接口的响应速度由15秒多,变成了1秒。但是过程中遇到坑了。特此记录一下

技术实现

优化过程

在这里插入图片描述

@Async 注解定义为可以放置在方法上和类上,当使用在类上表明类所有的方法都能异步执行。在Springboot中是需要在方法上加上该注解就可以完美的实现异步执行。

原始方法伪代码如下

/**
 * 原始代码 采用流水式的代码一步步实现 业务逻辑
 */
public void doBusiness(Object args){
  //1.  新增工单处理日志,如什么时间点将工单转移给某人
 
  //2. 修改工单当前处理人
  
  //3. 发送企业微信给B端的跟进人(转移人)
  
  //4. 发送im信息给C端的用户
}

那么异步问题就很好处理了,只需要将方法抽离形成多个子方法, 每个方法执行自己的业务处理逻辑,然后再方法加上@Async注解不就ok了么,伪代码如下

public void doBusiness(Object args){
 	//2. 修改工单当前处理人
  
  this.doAsyncBusiness();
}


// 单独抽离一个异步执行的方法 加上@Async注解
@Async
private void doAsyncBusiness(Object args){
  //1.  新增工单处理日志,如什么时间点将工单转移给某人
  
  //3. 发送企业微信给B端的跟进人(转移人)
  
  //4. 发送im信息给C端的用户
}

打完收工,重启应用,进行测试,然而并没有像预期中的那样,接口的响应速度还是15秒左右。接着排查原因,可以肯定的是@Async是可以提供异步方法执行。应该是我们使用方式不对导致。

@Async 限制

熟悉Springboot AOP的同学可能会发现更改后的代码存在明显的问题

  • 首先AOP代理机制要求 被代理的方法必须是 public , private 方法不能被代理
  • 其次AOP代理机制会生成一个代理类 执行代理方法 注意 this.doAsyncBusiness() 调用的是本对象的方法 ;
  • 在启动类上加上@EnableAsync注解

综上所述,原因我们已经通过AOP代理的原理找到了。下面摘自官方文档的一段话:

  • it must be applied to public methods only
  • self-invocation – calling the async method from within the same class – won’t work

The reasons are simple – 「the method needs to be public」 so that it can be proxied. And 「self-invocation doesn’t work」 because it bypasses the proxy and calls the underlying method directly.

### Java Spring Boot 中使用 Cursor 的常见问题与解决方案 在 Java Spring Boot 开发中,Cursor 是一种用于分页查询的技术手段,尤其适用于处理大规模数据集。以下是常见的问题及其对应的解决方案: #### 1. **NoClassDefFoundError 异常** 当尝试启用某些注解(如 `@Secured` 或其他 AOP 相关功能)时,可能会抛出 `NoClassDefFoundError` 异常。这是因为缺少必要的依赖项,例如 `spring-aop`。 **解决方案**: 确保项目中已引入 `spring-aop` 和相关依赖。可以在项目的 `pom.xml` 文件中添加以下内容[^1]: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` --- #### 2. **游标的初始化失败** 如果未正确配置数据库连接池或事务管理器,则可能导致 Cursor 初始化失败。 **解决方案**: 确认数据库连接池和事务管理器的配置无误。Spring Boot 默认支持多种数据库连接池(如 HikariCP),可以通过调整 `application.properties` 来优化设置: ```properties spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.connection-timeout=30000 ``` 此外,确保启用了事务管理器的支持: ```java @EnableTransactionManagement @Configuration public class TransactionConfig { } ``` --- #### 3. **内存溢出 (OutOfMemoryError)** 对于超大数据量的查询操作,一次性加载所有记录到内存中会引发内存不足的问题。 **解决方案**: 采用流式读取的方式逐步获取数据,而不是一次性加载全部结果。可以利用 JPA 提供的 `Stream<T>` 接口实现这一目标: ```java @Repository public interface MyRepository extends JpaRepository<MyEntity, Long> { @Query("SELECT e FROM MyEntity e WHERE e.status = :status") Stream<MyEntity> findEntitiesByStatus(@Param("status") String status); } // 调用方式 try (Stream<MyEntity> stream = myRepository.findEntitiesByStatus("ACTIVE")) { stream.forEach(entity -> process(entity)); } ``` 这种方式能够显著降低内存占用并提升性能[^2]。 --- #### 4. **游标关闭异常** 在实际开发过程中,忘记显式关闭 Cursor 可能导致资源泄漏或其他运行时错误。 **解决方案**: 始终通过 try-with-resources 块来自动释放资源: ```java try (ResultSet rs = statement.executeQuery()) { while (rs.next()) { // 处理每一行数据 } } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } ``` 此方法可有效防止因手动调用 `close()` 方法而引起的潜在问题。 --- #### 5. **并发访问冲突** 多线程环境下共享同一个 Cursor 实例容易造成竞争条件或数据一致性问题。 **解决方案**: 为每个线程创建独立的 Cursor 实例,并避免跨线程传递敏感对象。例如,在基于 Webflux 的异步场景下,推荐使用响应式编程模型替代传统阻塞式的 Cursor 操作: ```java Flux.fromIterable(myService.getData()) .flatMap(data -> Mono.just(processData(data))) .subscribe(result -> System.out.println(result)); ``` --- ### 总结 以上列举了几种典型的 Cursor 使用中的问题以及相应的解决策略。合理配置环境、优化代码逻辑以及遵循最佳实践可以帮助开发者更高效地完成任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值