性能文章>如何在SpringBoot中使用异步方法优化Service逻辑提高接口响应速度?>

如何在SpringBoot中使用异步方法优化Service逻辑提高接口响应速度?转载

187801

Part1为什么需要异步方法?

先说结论: 合理使用异步方法可以让业务接口快到飞起!

异步方法适用于逻辑与逻辑之间可以相互分割互不影响的业务中, 如生成验证码和发送验证码组成的业务, 其实无需等到真正发送成功验证码才对客户端进行响应, 可以让短信发送这一耗时操作转为异步执行, 解耦耗时操作和核心业务; 同理还有文章阅读的业务逻辑 = 查询文章详情 + 更新文章阅读量后再响应客户端, 其实也无需等到阅读量更新后才响应文章详情给客户端, 用户查看文章是主要逻辑, 而文章阅读量更新是次要逻辑, 况且阅读量就算更新失败一点数据偏差也不会影响用户阅读因此这两个数据库操作之间的一致性是较弱的, 这类都能用异步事件去优化. 所以说: 恰当的在我们的Service中加入异步方法能大大提高接口的响应速度, 提升用户体验!

同步执行(同在一个线程中):

异步执行(开启额外线程来执行):

Part2SpringBoot中的异步方法支持

其实, 在SpringBoot中并不需要我们自己去创建维护线程或者线程池来异步的执行方法, SpringBoot已经提供了异步方法支持注解.

@EnableAsync // 使用异步方法时需要提前开启(在启动类上或配置类上)
@Async // 被async注解修饰的方法由SpringBoot默认线程池(SimpleAsyncTaskExecutor)执行

比如使用Spring的异步支持实现文章查询并增加阅读量

Service层:

@Service
public class ArticleServiceImpl {

    // 查询文章
    public String selectArticle() {
        // TODO 模拟文章查询操作
        System.out.println("查询任务线程"+Thread.currentThread().getName());
        return "文章详情";
    }

    // 文章阅读量+1
    @Async
    public void updateReadCount() {
        // TODO 模拟耗时操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("更新任务线程"+Thread.currentThread().getName());
    }
}

Controller层:

@RestController
public class AsyncTestController {

    @Autowired
    private ArticleServiceImpl articleService;

    /**
     * 模拟获取文章后阅读量+1
     */
    @PostMapping("/article")
    public String getArticle() {
        // 查询文章
        String article = articleService.selectArticle();
        // 阅读量+1
        articleService.updateReadCount();
        System.out.println("文章阅读业务执行完毕");
        return article;
    }
}

测试结果: 我们可以感受到接口响应速度大大提升, 而且从日志中key看到两个执行任务是在不同的线程中执行的

Part3自定义线程池执行异步方法

SpringBoot为我们默认提供了线程池(SimpleAsyncTaskExecutor)来执行我们的异步方法, 我们也可以自定义自己的线程池.

第一步配置自定义线程池

@EnableAsync // 开启多线程, 项目启动时自动创建
@Configuration
public class AsyncConfig {
    @Bean("customExecutor")
    public ThreadPoolTaskExecutor asyncOperationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(8);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 设置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("AsyncOperationThread-");
        executor.setThreadGroupName("AsyncOperationGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }
}

第二步, 在@Async注解上指定执行的线程池即可

// 文章阅读量+1
@Async("customExecutor")
public void updateReadCount() {
    // TODO 模拟耗时操作
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("更新文章阅读量线程"+Thread.currentThread().getName());
}

Part4如何捕获(无返回值的)异步方法中的异常

  • 以实现AsyncConfigurer接口的getAsyncExecutor方法和getAsyncUncaughtExceptionHandler方法改造配置类
  • 自定义异常处理类CustomAsyncExceptionHandler
@EnableAsync // 开启多线程, 项目启动时自动创建
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(8);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 设置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("AsyncOperationThread-");
        executor.setThreadGroupName("AsyncOperationGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
 
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.out.println("异常捕获---------------------------------");
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.out.println("Parameter value - " + param);
        }
        System.out.println("异常捕获---------------------------------");
    }    
}

Part5如何获取(有返回值)异步方法的返回值

使用Future类及其子类来接收异步方法返回值 注意:

  • 无返回值的异步方法抛出异常不会影响Controller的主要业务逻辑
  • 有返回值的异步方法抛出异常会影响Controller的主要业务逻辑
// 异步方法---------------------------------------------------------------------
@Async
    public CompletableFuture<Integer> updateReadCountHasResult() {
        // TODO 模拟耗时操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("更新文章阅读量线程"+Thread.currentThread().getName());
        return CompletableFuture.completedFuture(100 + 1);
    }

// Controller调用---------------------------------------------------------------------
@GetMapping("/article")
public String getArticle() throws ExecutionException, InterruptedException {
    // 查询文章
    String article = articleService.selectArticle();
    // 阅读量+1
    CompletableFuture<Integer> future = articleService.updateReadCountHasResult();
    int count = 0;
    // 循环等待异步请求结果
    while (true) {
        if(future.isCancelled()) {
            System.out.println("异步任务取消");
            break;
        }
        if (future.isDone()) {
            count = future.get();
            System.out.println(count);
            break;
        }
    }
    System.out.println("文章阅读业务执行完毕");
    return article + count;
}

Part6异步方法带来的问题/拓展

  • 异步方法只能声明在Service方法中在Controller直接调用才会生效, 异步方法被同级Service方法调用不会生效, 很奇怪?
  • 异步方法 + 事务能顺利执行吗? 或许事务操作应该和异步操作分离开, 被Controller层调用时事务操作在前, 异步操作在后
  • 异步方法执行失败后对Controller前半部分的非异步操作无影响, 因此说异步方法在整个业务逻辑中不是100%可靠的, 对于强一致性的业务来说不适用
  • 还是消息中间件更为强大, RabbitMQ, Kafka…

作者:jinchange

分类:
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

大数据中台之Kafka,到底好在哪里?
Hello,大家好,今天给大家分享一个大数据里面很火的技术——Kafka,Kafka 是一个分布式的消息系统,其高性能在圈内很出名。本人阅读过多个大数据生态的开源技术的源码,个人感觉 Kafka 的源
什么?搞不定Kafka重复消费?
今天我们聊一个话题,如何保证 Kafka 消息不重复消费?在使用 Kafka 的时候一般都会设置重试的次数,但是因为网络的一些原因,设置了重试就有可能导致有些消息重复发送了(当然导致消息重复也有可能是其他原因),那么怎么解决消息重复这个问题呢?
Kafka的生产者优秀架构设计
前言 Kafka 是一个高吞吐量的分布式的发布订阅消息系统,在全世界都很流行,在大数据项目里面使用尤其频繁。笔者看过多个大数据开源产品的源码,感觉 Kafka 的源码是其中质量比较上乘的一个,这得益于
一次 Docker 容器内大量僵尸进程排查分析
前段时间线上的一个使用 Google Puppeteer 生成图片的服务炸了,每个 docker 容器内都有几千个孤儿僵死进程没有回收,如下图所示。这篇文章比较长,主要就讲了下面这几个问题。- 什么情
What?一个 Dubbo 服务启动要两个小时!
前言前几天在测试环境碰到一个非常奇怪的与 ```dubbo``` 相关的问题,事后我在网上搜索了一圈并没有发现类似的帖子或文章,于是便有了这篇。希望对还未碰到或正在碰到的朋友有所帮助。 现象现象是这样
Jackson修改字段名和自定义命名策略
国庆期间写了一教程:[《轻松学习Jackson》程序员口袋里的开发手册](https://996geek.com/articles/164),这是其中的一篇。Jackson支持在处理数据的时候,使用不
又踩到Dubbo的坑,但是这次我笑不出来
前言直入主题,线上应用发现,偶发性出现如下异常日志。当然由于线上具体异常包含信息量过大,秉承让肥朝的粉丝没有难调试的代码的原则,我特意抽取了一个复现的demo放在了git,让你不在现场,一样享受到排查
kill -9 导致 Kakfa 重启失败的惨痛经历!
背景在 2 月10 号下午大概 1 点半左右,收到用户方反馈,发现日志 kafka 集群 A 主题 的 34 分区选举不了 leader,导致某些消息发送到该分区时,会报如下 no leader 的错