我前面写了一篇springboot优雅shutdown的文章,看起来一切很美好。
https://blog.csdn.net/chenshm/article/details/139640775
那是因为没有进行多线程测试。如果一个请求中包括阻塞线程(主线程)和非阻塞线程(异步线程),会是什么效果?接下来我们就测试一番。
1. 验证优雅shutdown的异步线程安全性
- 确认graceful shutdown配置
查看源码可以看到springboot graceful shutdown默认只会等待30s,我这里设置更长的时间只是方便测试,实际设置还是需要根据你业务api最长执行时间来配置。
- 准备测试代码
/**
* @Author 公众号: IT三明治
* @Date 2024/6/15
* @Description:
*/@Slf4j@RestController@RequestMapping("/api")publicclassDemoController{@GetMapping("/{userId}")publicResultVo<Object>getUserInfo(@PathVariableString userId)throwsInterruptedException{
log.info("userId:{}", userId);Runnable runnable =()->{for(int i =0; i <60; i++){
log.info("async thread to update user login info to other services, service num: {}", i);try{Thread.sleep(1000);}catch(InterruptedException e){thrownewRuntimeException(e);}}};Thread thread =newThread(runnable);
thread.start();for(int i =0; i <30; i++){
log.info("querying user info for {}, waiting times: {}", userId, i);Thread.sleep(1000);}returnResultVo.ok();}}
这里我设置非阻塞线程的循环是60次,大概60s完成,阻塞线程循环只有30次,大概30s完成。主要是为了测试我的阻塞线程完成后,graceful shutdown能不能保证我的异步线程安全。
- 请求api
Administrator@USER-20230930SH MINGW64 /d/git/micro-service-logs-tracing
$ curl http://localhost:8080/api/sandwich
- shutdown app(Ctrl+F2)
- 查看日志
可以看到shutdown信号发出之后,两个线程都还在跑,但是阻塞线程(0-29)结束之后,异步线程也跟着终结了。它的循环应该是从0到59才算结束,但是只跑到30,所以异步线程是不安全的。
- 验证主线程返回结果 阻塞线程还是安全的,response正常返回了。
其实这种测试方法并不局限于解决springboot的问题,其他微服务也是类似的。过去我看到一些朋友测试release的安全性,只是不断call health api,只要release 期间health api没有返回异常就当作ok了,其实这只能验证你的负载均衡服务的可靠性,你自己app的安全问题还是没有得到解决。
既然问题找到了,接下来我来解决它。
2. 确保优雅shutdown app时异步线程也安全
2.1 优化代码
前面的异步线程只是简单地写个野线程,并不规范,我先优化一下。
- 把野线程放到线程池执行;
- 利用mbean的PreDestroy来在servcie销毁前先等待异步线程完成;
- 利用ExecutorService 的awaitTermination方法预判断异步线程的最长等待时间,等待异步线程完成,如果线程没有按时完成再强制结束。
/**
* @Author 公众号: IT三明治
* @Date 2024/6/15
* @Description:
*/@Slf4j@ServicepublicclassAsyncServiceImplimplementsAsyncService{privatefinalExecutorService executorService;publicAsyncServiceImpl(){this.executorService =Executors.newFixedThreadPool(10);}@OverridepublicvoidfeedUserInfoToOtherServices(String userId){
executorService.execute(()->{for(int i =0; i <35; i++){
log.info("async thread to update {} login info to other services, service num: {}", userId, i+1);try{Thread.sleep(1000);}catch(InterruptedException e){thrownewRuntimeException(e);}}});}@PreDestroypublicvoidtearDown(){if(null!= executorService){
executorService.shutdown();try{if(!executorService.awaitTermination(50,TimeUnit.SECONDS)){
executorService.shutdownNow();}}catch(InterruptedException e){
log.info("PreDestroy executorService is interrupted", e);
executorService.shutdownNow();}}}}
api代码调整如下
/**
* @Author 公众号: IT三明治
* @Date 2024/6/15
* @Description:
*/@Slf4j@RestController@RequestMapping("/api")publicclassDemoController{@ResourceAsyncService asyncService;@GetMapping("/{userId}")publicResultVo<Object>getUserInfo(@PathVariableString userId)throwsInterruptedException{
log.info("userId:{}", userId);
asyncService.feedUserInfoToOtherServices(userId);for(int i =0; i <30; i++){
log.info("updating user info for {}, waiting times: {}", userId, i+1);Thread.sleep(1000);}returnResultVo.ok();}}
2.2 验证shutdown过程异步线程的安全
从新代码看来,我们期待的结果是一个api请求,主线程循环从1到30,异步线程是从1到35,主线程先完成,异步线程会在AsyncServiceImpl servcie bean销毁前先等待异步线程完成。接下来是验证步骤。
- 重启服务
- call api
Administrator@USER-20230930SH MINGW64 /d/git/micro-service-logs-tracing
$ curl http://localhost:8080/api/sandwich
- shutdown app(Ctrl + F2)
- 查看日志
分析日志发现一切如代码所料,app graceful shutdown的时候,异步线程的安全性得到保障。
这个过程看起来非常完美,其实还不够完美,解决方案没最好,只有更好。请先关注我,容我研究一下,下期告诉你为什么。
版权归原作者 IT三明治 所有, 如有侵权,请联系我们删除。