springboot优雅shutdown时异步线程安全优化

打印 上一主题 下一主题

主题 848|帖子 848|积分 2544

前面针对graceful shutdown写了两篇文章
第一篇:
https://blog.csdn.net/chenshm/article/details/139640775
只思量了阻塞线程,没有思量异步线程
第二篇:
https://blog.csdn.net/chenshm/article/details/139702105
第二篇思量了多线程的安全性,包括异步线程。
1. 为什么还需要优化呢?

因为第二篇的写法还不够优美,它存在以下缺陷。


  • 只在一个service bean 里面临ExecutorService做predestroy,只能对一个service类的异步线程提供安全保障,其他service类的异步业务需要重写predestroy的逻辑,造成代码冗余。
  • 异步方法的写法比较贫苦,其他程序员并不常用。如今用springboot的程序员喜欢用@Async注解,随时随地可以把方法酿成异步执行。
    从架构师的角度思量的话,写代码只管满意多数环境可用,易用,最好照旧全局有效的,让其他程序员专注于写业务代码。
    接下来让我们实现@Async注解的异步方法在app graceful shutdow时保持线程安全。
2. 代码优化



  • 确认graceful shutdown settings

  • 添加第一个servcie 的异步方法
  1. package com.it.sandwich.service.impl;
  2. import com.it.sandwich.service.Demo2Service;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.scheduling.annotation.Async;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.stereotype.Service;
  7. /**
  8. * @Author 公众号: IT三明治
  9. * @Date 2024/6/16
  10. * @Description:
  11. */
  12. @Slf4j
  13. @Service
  14. @Component
  15. public class Demo2ServiceImpl implements Demo2Service {
  16.     @Override
  17.     @Async
  18.     public void feedUserInfoToOtherService(String userId) throws InterruptedException {
  19.         for (int i = 0; i < 40; i++) {
  20.             log.info("Demo2Service update {} login info to other services, service num: {}", userId, i+1);
  21.             Thread.sleep(1000);
  22.         }
  23.     }
  24. }
复制代码


  • 添加第二个Servcie 的异步方法
  1. package com.it.sandwich.service.impl;
  2. import com.it.sandwich.service.Demo2Service;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.scheduling.annotation.Async;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.stereotype.Service;
  7. /**
  8. * @Author 公众号: IT三明治
  9. * @Date 2024/6/16
  10. * @Description:
  11. */
  12. @Slf4j
  13. @Service
  14. @Component
  15. public class Demo1ServiceImpl implements Demo1Service {
  16.     @Override
  17.     @Async
  18.     public void feedUserInfoToOtherService(String userId) throws InterruptedException {
  19.         for (int i = 0; i < 35; i++) {
  20.             log.info("Demo1Service update {} login info to other services, service num: {}", userId, i+1);
  21.             Thread.sleep(1000);
  22.         }
  23.     }
  24. }
复制代码
添加两个@Async方法,验证全局生效。


  • api接口
  1. package com.it.sandwich.controller;
  2. import com.it.sandwich.base.ResultVo;
  3. import com.it.sandwich.service.Demo1Service;
  4. import com.it.sandwich.service.Demo2Service;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.PathVariable;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import javax.annotation.Resource;
  11. /**
  12. * @Author 公众号: IT三明治
  13. * @Date 2024/6/16
  14. * @Description:
  15. */
  16. @Slf4j
  17. @RestController
  18. @RequestMapping("/api")
  19. public class DemoController {
  20.     @Resource
  21.     Demo1Service demo1Service;
  22.     @Resource
  23.     Demo2Service demo2Service;
  24.     @GetMapping("/{userId}")
  25.     public ResultVo<Object> getUserInfo(@PathVariable String userId) throws InterruptedException {
  26.         log.info("userId:{}", userId);
  27.         demo1Service.feedUserInfoToOtherService(userId);
  28.         demo2Service.feedUserInfoToOtherService(userId);
  29.         for (int i = 0; i < 30; i++) {
  30.             log.info("updating user info for {}, waiting times: {}", userId, i+1);
  31.             Thread.sleep(1000);
  32.         }
  33.         return ResultVo.ok();
  34.     }
  35. }
复制代码


  • @Async有效的全局线程池设置
  1. package com.it.sandwich.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.scheduling.annotation.EnableAsync;
  5. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  6. import java.util.concurrent.Executor;
  7. /**
  8. * @Author 公众号: IT三明治
  9. * @Date 2024/6/16
  10. * @Description:
  11. */
  12. @Configuration
  13. @EnableAsync
  14. public class AsyncConfig {
  15.     @Bean
  16.     public Executor taskExecutor() {
  17.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  18.         executor.setCorePoolSize(2); // 设置核心线程数
  19.         executor.setMaxPoolSize(5); // 设置最大线程数
  20.         executor.setQueueCapacity(100); // 设置队列容量
  21.         executor.setThreadNamePrefix("sandwich-async-pool-"); // 自定义线程名称前缀
  22.         executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程池关闭时是否等待任务完成
  23.         executor.setAwaitTerminationSeconds(60); // 设置等待时间,如果你需要所有异步线程的安全退出,请根据线程池内敢长线程处理时间配置这个时间
  24.         return executor;
  25.     }
  26. }
复制代码
3. 验证代码



  • 重启服务
  • call api
  1. Administrator@USER-20230930SH MINGW64 /d/git/micro-service-logs-tracing
  2. $ curl http://localhost:8080/api/sandwich
复制代码


  • shutdown app(Ctrl+F2)
  • 验证日志
    查看日志前我们先分析一下代码,我们一个api请求里面一共有三个线程,一个阻塞线程,两个@Async注解修饰的异步线程。阻塞线程的循环计数日志从1到30,Demo1Service 异步线程的循环计数日志从1到35,Demo2Service异步线程的循环计数日志从1到40。我们期待的结果是提前shutdown之后三个线程的计数日志都完备打印出来。

日志完美验证了我们的期待。 我设置的“sandwich-async-pool-”线程名前缀也在两个线程日志中体现了。进一步证明AsyncConfig对全部@Async注解修饰的异步线程全局有效。
这是为什么呢?
4. AsyncConfig设置代码分析

当我在Spring设置中通过@Bean界说了一个ThreadPoolTaskExecutor实例,而且在同一设置类或其他被扫描到的设置类中启用了@EnableAsync注解时,这个自界说线程池会自动与Spring的异步任务执行机制关联起来。这一过程背后的原理涉及到Spring的异步任务执行器(AsyncConfigurer接口)的自动设置和代理机制,具体原因如下:
   

  • Spring的自动装配(Auto Configuration): Spring Boot利用自动设置(auto-configuration)机制来简化设置。当它检测到@EnableAsync注解时,会自动寻找并设置一个TaskExecutor(线程池)来执行@Async标记的方法。假如在应用上下文中存在多个TaskExecutor的Bean,Spring通常会选择一个合适的Bean作为默认的异步执行器。自界说的ThreadPoolTaskExecutor Bean由于是明确设置的,因此优先级较高,天然成为首选。
  • AsyncConfigurer接口: 当我使用@EnableAsync时,现实上是在告诉Spring去查找实现了AsyncConfigurer接口的设置类。假如我没有直接实现这个接口并提供自界说设置,Spring会使用默认的设置。但是,假如我提供了自界说的ThreadPoolTaskExecutor Bean,Spring会以为这是我盼望用于异步任务的线程池。
  • Spring AOP代理: @Async注解的方法在运行时会被Spring的AOP(面向切面编程)机制代理。这个代理逻辑会检查是否有设置好的TaskExecutor,假如有(好比我自界说的ThreadPoolTaskExecutor),就会使用这个线程池来执行方法,从而实现了异步调用。
  • Bean的命名和范例匹配: 默认环境下,Spring在查找执行器时会优先思量那些名为taskExecutor的Bean,这也是为什么在设置ThreadPoolTaskExecutor时通常会使用这个名字。当然,即使不叫这个名字,也可以通过实现AsyncConfigurer接口并重写getAsyncExecutor方法来指定使用的线程池。
  综上所述,自界说的ThreadPoolTaskExecutor之以是能成为Spring异步任务执行的默认线程池,是因为Spring的自动设置逻辑、AOP代理机制以及通过设置明确指定了这个线程池的使用。
至此,graceful shutdown已经可以使多线程,高并发的项目在做release的时候,线程安全性得到保障。 特别是一些长处置惩罚的schedul job项目(此中好多job为了提交效率,用了异步机制),经过这样优化之后,release的信心是不是加强了好多。
写文章不容易,假如对您有用,请点个关注支持一下博主再走。谢谢。
假如有更好见解的朋友,请在评论区给出您的引导意见,感谢!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

冬雨财经

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表