SpringBoot @Async如何优雅的异步调用

应用场景

当我们登录系统的时候,我们的业务主要是验证账号和密码,而和登录影响不大的其它业务,例如:

  • 发送邮箱
  • 发送短信登录提醒
  • 发送系统登录日志

等等其他业务操作。我们为了用户的体验,我们可以将其它的业务操作放到子线程中在后台慢慢执行。

众所周知,程序的运行默认是从上而下的单线程运行,当我们需要执行一个异步操作的时候,就需要手动的创建一个新的线程。一般我们的操作如下

public class ThreadTest {

    public static void main(String[] args) {

//        这里我实现一个Runnable接口的实现
        Runnable runnable=()->{
            long start = System.currentTimeMillis();    // 获取线程的开始执行时间
            try {
                Thread.sleep(3000);     // 线程沉睡3秒中,便于观察
                System.out.println("异步执行一个方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = System.currentTimeMillis();  // 获取线程的结束时间
            System.out.println(Thread.currentThread().getName()+", "+(end-start)+"毫秒");
        };

//        这里我们开启两个线程相互执行
        Thread thread = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread.start();     // 开启线程一
        thread2.start();    // 开启线程二
    }
}

在这里插入图片描述
控制台打印的时候是同时打印的,也就是说我们两个线程执行的时间一共是3+秒。当然我们也可以通过线程池中取得线程执行,这边就不进行展开了。

这篇文章我们讨论的重点就是:spring 3.0之后推出的一个异步注解的形式,如何优雅的开启异步调用。

代码测试

全局配置

新建一个全局的配置类,开启一个线程池,要想@Async这个注解起作用,必须加@EnableAsync这个注解,异步功能模块的功能加载。

@Async会默认从线程池获取线程,当然也可以显式的指定@Async(“thread-pool”)

关于为什么使用线程池的优点,我就不过多介绍了,无非一下几点

  • 节省性能开销,程序不用频繁的创建线程,和销毁线程。
  • 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
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 org.mybatis.spring.annotation.MapperScan;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
@MapperScan("com.example.mapper")// 扫描mapper
public class ThreadPoolConfig {

    @Bean("thread-pool") 
    public ThreadPoolTaskExecutor threadPool(){
        ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
        pool.setCorePoolSize(50);   //配置核心线程的大小
        pool.setMaxPoolSize(200);   //配置线程池的最大线程数
        // 线程池维护线程所允许的空闲时间
        pool.setKeepAliveSeconds(300);
        // 队列最大长度
        pool.setQueueCapacity(1000);
        // 线程池对拒绝任务(无线程可用)的处理策略
        pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return pool;
    }
}

配置好后,@Async会默认从线程池获取线程,当然也可以显式的指定@Async(“asyncTaskExecutor”)。

1、业务层接口
public interface IAsyncService {
   /**
     * 无返回值
     */
    @Async
    void insert();

    /**
     * 有返回值
     * @param name
     * @return
     */
    @Async
    Future<String> delete(String name);

    /**
     * 有返回值,测试事务
     * @param to
     * @return
     */
    @Async
    void update(Boolean to);
}
2、业务层实现

完全可以看到我实现了三个接口,每个接口我都模拟成执行2秒中结束。

import com.example.future.IAsyncFutureService;
import com.example.mapper.UserMapper;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.Future;

@Service
public class AsyncFutureServiceImpl implements IAsyncFutureService {

    /**
     * 加入datasource的 mapper
     *  为了测试事务回滚,加入的数据代理模型
     */
    @Autowired
    private UserMapper userMapper;

    @Async
    @Override
    public void insert() {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
            System.out.println("2秒执行任务insert");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ", " + (end - start) + "毫秒");
    }

    @Async
    @Override
    public Future<String> delete(String name) {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ", " + (end - start) + "毫秒");
        return AsyncResult.forValue(name);
    }

    @Async
    @Override
    @Transactional 	//开启事务
    public void update(Boolean to) {
        User user = new User();
        user.setName("张三");
        userMapper.insertSelective(user);
//        模拟异常,事务回滚
        if (to) {
            throw new RuntimeException("模拟异常");
        }
    }
}
测试异步调用

将异步的调用放到spring容器中,程序启动的时候自动加载,这个方便我测试,免去了从外部请求的流程。

注意一下几点bug:

  1. 我们将异步Manager (就是下边的bean)、异步的业务层,分开放置。用异步Manager,调用业务层里边的逻辑。如果没有分开放置,异步调用不会起作用。
  2. 真实的开发环境的使用,肯定不会程序已启动就加载异步Manager,而是控制层进行调用的这个异步Manager。
1、无返回值
  @Component
public class Manager {

//    @Autowired
//    private IAsyncService asyncService;

    @Autowired
    private IAsyncFutureService futureService;

    /**
     * 无返回值,异步调用
     */
//    @Bean
    public ApplicationRunner applicationInsert() {
        return applicationArguments -> {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");

            //无返回值
            futureService.insert();

            long endTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
        };
    }
}

在这里插入图片描述

2、有返回值
/**
     * 有返回值,异步调用
     */
    @Bean
    public ApplicationRunner applicationDelete() {
        return applicationArguments -> {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");

            //返回值
            Future<String> delete = futureService.delete("异步调用,有返回值输出");
            String s = delete.get();
            System.out.println("返回值:"+s);
            long endTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
        };
    }

在这里插入图片描述

3、事务回滚
# 打印调试日志,方便观察
logging:
  level:
   root:
    debug
 /**
     * 启动成功
     */
    @Bean
    public ApplicationRunner applicationRunner() {
        return applicationArguments -> {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
            //事务测试,模拟异常事务回滚
            futureService.update(true);

            long endTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
        };
    }

如图可以看到,我们的业务抛出了一个异常,并执行了事务回滚
在这里插入图片描述

看到数据库中并没有加进去数据,说明我们的事务已经起作用了
在这里插入图片描述

模拟真实业务登录场景

我们登录的时候,除了输入登录的账号密码外,我再加一个需求:保存登录的日志

1、 前端控制器分发任务

请求地址发送到控制器中,分发任务到各个业务接口。

{
    "name":"zhangsan"
}
import com.example.db.service.UserService;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

//    登录接口
    @RequestMapping("/login")
    public void login(@RequestBody User user){
        userService.login(user);
    }
}
2、用户登录业务接口实现

用户的登录业务逻辑模拟,引入异步manager,异步调用log日志创建

import com.example.Manager;
import com.example.db.service.UserService;
import com.example.mapper.UserMapper;
import com.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private Manager manager;

    @Override
    public void login(User user){
        userMapper.insertSelective(user);

//        异步manager 调用log服务
        manager.logInsert(user.getId());
    }
}
3、异步manager

调用log异步服务,如果有其他的业务也是写在该方法下边一起异步执行登录的逻辑,例如发邮件,发登录短信提醒等。

    public void logInsert(Integer userID){
        long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ":开始调用异步业务");
        try {
            Thread.sleep(3000);
            Log log = new Log();
            log.setUserId(userID);
            logService.insert(log);
            long endTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + ":调用异步业务结束,耗时:" + (endTime - startTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
4、日志业务接口实现

可以看到我们这里加入了@Async注解开启了异步执行的通道。

import com.example.db.service.LogService;
import com.example.mapper.LogMapper;
import com.example.model.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogMapper logMapper;

    @Async
    @Override
    public void insert(Log log){
        log.setText("欢迎用户:"+log.getUserId()+"登录成功");
        logMapper.insertSelective(log);
    }
}

这里只有一个日志总共执行了3秒多,如果有其他的业务放在manager中的logInsert()方法总的执行时间也是在3秒+,原因是我的是异步调用,每个服务都是一个线程独立执行,互不干涉。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

平平常常一般牛

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

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

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

打赏作者

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

抵扣说明:

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

余额充值