likes
comments
collection
share

异步注解@Async避坑指南

作者站长头像
站长
· 阅读数 6

引言

我们平时在项目开发过程中,为了提高接口性能,经常会用到多线程异步处理,其中使用@Async注解就是异步处理的其中一种方式。@Async注解是Spring为提供的关键工具,它可以标记在一个方法上,使得该方法在调用时在单独的线程中执行。然而,在实际应用中,我们需要对@Async的使用有一些深入的理解和注意事项,下面我们就一起来探索如何有效、安全地使用@Async,并避开其中的一些常见陷阱。

@Async注解基础使用

1、启动类要加上@EnableAsync注解,否则异步不生效

2、在任何Service或者Component类的方法上使用@Async注解

直接上代码:

package com.example.springbootdemo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@Slf4j
@EnableAsync
public class SpringbootdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootdemoApplication.class, args);
        log.info("project start sucess!");
    }
}
package com.example.springbootdemo.service;

import com.example.springbootdemo.pojo.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {

    private final UserService userService;

    /**
     * 查询所有用户信息
     *
     * @return 返回所有用户信息
     */
    @RequestMapping("/findAllUser")
    public List<User> findAllUser() {
        long start = System.currentTimeMillis();
        // 异步添加操作日志
        userService.addOperationLog();
        List<User> allUser = userService.findAllUser();
        log.info("findUser end.time:{}", System.currentTimeMillis() - start);
        return allUser;
    }
}

package com.example.springbootdemo.service;

import com.example.springbootdemo.pojo.User;
import java.util.List;

public interface UserService {

    List<User> findAllUser();

    void addOperationLog();
}
package com.example.springbootdemo.service;

import com.example.springbootdemo.dao.UserDao;
import com.example.springbootdemo.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public List<User> findAllUser() {
        return userDao.findAllUser();
    }

    @Override
    @Async
    public void addOperationLog() {
        userDao.addLog();
        try {
          // 休眠10秒
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.springbootdemo.dao.UserDao">
    <insert id="addLog">
        insert into log (content, createTime) values ('查询用户信息',now())
    </insert>
    <!--查询所有用户-->
    <select id="findAllUser" resultType="com.example.springbootdemo.pojo.User">
        SELECT * FROM user
    </select>
</mapper>

运行效果:

异步注解@Async避坑指南

以上代码为注解正常生效的场景,接口执行时间为295毫秒,说明是异步执行了addOperationLog方法

这里列举两种非常常见容易犯错,注解不生效的场景

1、调用方和@Async能在一个类中

我们把代码改一改,把原来直接调用userService.addOperationLog()抽取到当前类中一个方法中

package com.example.springbootdemo.service;

import com.example.springbootdemo.pojo.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {
    private final UserService userService;

    /**
     * 查询所有用户信息
     *
     * @return 返回所有用户信息
     */
    @RequestMapping("/findAllUser")
    public List<User> findAllUser() {
        long start = System.currentTimeMillis();
        // 异步添加操作日志
        addOperationLog();
        List<User> allUser = userService.findAllUser();
        log.info("findUser end.time:{}", System.currentTimeMillis() - start);
        return allUser;
    }
  
    @Async
    public void addOperationLog() {
        userService.addOperationLog();
    }
}

运行效果:

异步注解@Async避坑指南

接口执行了10294毫秒,findAllUser操作在addLog之后执行,说明异步不生效

原因:

  • 如果在同一个类内部调用带@Async注解的方法,由于Spring AOP基于代理机制,默认使用的JDK动态代理或CGLIB代理只对类外部的调用起作用。所以,当同一个类内部调用时,实际上是绕过了代理,从而导致@Async注解无效。

2、启动类没有加@EnableAsync注解

异步注解@Async避坑指南

从运行结果看出,同样是不生效

@Async进阶使用

该注解是通过一个默认的线程池来异步执行方法,是spring-boot里面的TaskExecutionProperties的一个静态内部类,如果需要修改默认的配置可以在yaml或者properties中添加

默认的线程池配置:

异步注解@Async避坑指南

使用springboot默认的线程池在某些场景下可能存在一些不足之处:

  1. 默认配置可能不适合特定业务场景

    • 默认线程池的大小、队列容量、线程存活时间等参数都是基于通用场景的预估值,可能并不适合所有的应用负载。例如,对于高并发场景,默认的线程池大小可能过小,而对于IO密集型任务,可能需要更大的队列容量或更长的线程存活时间。
  2. 资源浪费或不足

    • 如果默认的最大线程数过大,可能会消耗过多系统资源(如内存、CPU),特别是在服务器资源有限的情况下。相反,如果默认值过小,则可能在高峰负载期间无法充分利用系统资源处理更多任务,导致响应延迟。
  3. 任务积压和响应能力

    • 默认配置可能会使用无界队列,这意味着任务会一直堆积,而不考虑系统能否及时处理。在严重情况下,这可能导致内存溢出,因为队列中保存了大量待处理的任务,而且如果线程池中的线程都被占满且无法快速释放,新的请求可能会得不到及时响应。
  4. 异常处理和监控

    • 默认线程池可能没有配置完善的异常处理和监控机制,这对于生产环境来说是很重要的。自定义线程池可以方便地集成度量、告警和日志记录等功能。
  5. 事务边界问题

    • 异步方法默认不在原始事务上下文中执行,如果业务逻辑需要跨多个方法组合并保持事务一致性,就需要特殊处理。使用默认线程池时,开发者可能忽略这一点,导致数据一致性问题。

因此,建议针对具体的业务场景和技术要求,对线程池进行定制化的配置和优化,以确保系统在性能、资源利用、可扩展性等方面表现出色。

自定义线程池:

@Configuration
public class ThreadPoolConfig {
    @Value("$(user.task.execution.pool.queueCapacity:8}")
    private Integer queueCapacity;
    @Value("$fuser.task.execution.pool.coresize:8)")
    private Integer corePoolSize;
    @Value("$(user.task.execution.pool.maxSize:8)")
    private Integer maxPoolSize;
    @Value("$(user.task.execution.pool.keepAlive:5)")
    private Integer keepAlive;
    @Value("$(user.task.execution.threadNamePrefix:user-task-]")
    private String threadNamePrefix;

    @Bean("userAsyncExecutor")
    public Executor userAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(corePoolSize);

        // 设置最大线程数
        executor.setMaxPoolSize(maxPoolSize);

        // 设置队列容量
        executor.setQueueCapacity(queueCapacity);

        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(keepAlive);

        // 设置线程工厂
        executor.setThreadNamePrefix(threadNamePrefix);

        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 初始化
        executor.initialize();
        return executor;
    }

}

线程池使用:

异步注解@Async避坑指南

运行效果:

异步注解@Async避坑指南

异步注解@Async避坑指南

@Async不生效汇总

  1. 缺少@EnableAsync注解

    • 在Spring配置类上未添加@EnableAsync注解,这是启用异步处理的基础。没有这个注解,Spring将不会为带有@Async的方法创建代理,因此异步方法将同步执行。
  2. 调用方式问题(最容易踩的坑)

    • 如果在同一个类内部调用带@Async注解的方法,由于Spring AOP基于代理机制,默认使用的JDK动态代理或CGLIB代理只对类外部的调用起作用。所以,当同一个类内部调用时,实际上是绕过了代理,从而导致@Async注解无效。解决办法是通过另一个类(通常是注入的服务)来调用异步方法。
  3. Spring Bean生命周期问题

    • 如果带有@Async注解的方法是在一个非Spring托管的Bean中,或者是通过new关键字直接创建的对象的方法,那么Spring AOP无法对其进行增强,因此异步注解也不会生效。
  4. 方法签名限制

    • @Async注解的方法必须满足一定的返回类型要求,一般应该是void或者java.util.concurrent.Future的某种形式。若方法返回类型不符合规定,异步特性将不会生效。
  5. 线程池配置错误或未配置

    • 若没有正确配置线程池或者线程池未启动,虽然@Async注解被识别,但是任务无法投递到线程池中执行,看起来就像是异步注解未生效一样。
  6. Spring Boot环境下的自动配置问题

    • 在Spring Boot环境中,如果依赖的starter包版本不支持自动配置异步方法,或者相关配置被禁用或覆盖,也可能导致@Async注解不生效。
  7. Spring Security的影响

    • 在特定情况下,Spring Security可能会干扰异步方法的正常执行,尤其是在方法级别Security配置的情况下。