1.前言
近日就系统重启引发了一些思考,在系统重启过程中,正在进行的请求会如何被处理?正在消费的消息会不会丢失?异步执行的任务会不会被中断?既然存在这些问题,那我们的应用程序是不是就不能重启?但是,我们的应用程序随着版本迭代也在不断重启为什么这些问题没有出现呢?还是应用做了额外处理?带着这些疑问,结合场景模拟,看看实际情况怎么处理。
2. 场景
2.1 http请求
2.1.1 创建请求
@RestControllerpublicclassShutDownController{@RequestMapping("shut/down")publicStringshutDown()throwsInterruptedException{TimeUnit.SECONDS.sleep(20);return"hello";}}
2.1.2 调用请求
http://localhost:8080/shut/down
2.1.3 模拟重启
kill-2应用pid
2.1.4 现象
2.1.5 结论
请求执行过程中,关闭应用程序出现无法访问提示
2.1.6 开启优雅关机
如上出现的现象对用户来说很不友好,会造成用户一脸懵逼,那么有没有什么措施可以避免这种现象的出现呢?是否可以在应用关闭前执行完已经接受的请求,拒绝新的请求呢?答案可以的,只需要在配置文件中新增优雅关机
配置
server:shutdown:graceful#设置优雅关闭,该功能在SpringBoot2.3版本中才有。注意:需要使用Kill-2触发来关闭应用,该命令会触发shutdownHookspring:lifecycle:timeout-per-shutdown-phase:30s#设置缓冲时间,注意需要带上时间单位(该时间用于等待任务执行完成)
添加完配置后,再次执行2.1.2
和2.1.3
流程,就会看到如下效果
可以看到,即便在请求执行过程中关闭应用,已接收的请求依然会执行下去
2.2 消息消费
在前言
提到过,消息消费过程中,关闭应用,消息是会丢失还是会被重新放入消息队列中呢?
2.2.1 创建生产者
@RestControllerpublicclassRabbitMqController{@AutowiredprivateRabbitTemplaterabbitTemplate;@GetMapping("/sendBusinessMessage")publicvoidsendBusinessMessage()throwsInterruptedException{rabbitTemplate.convertAndSend(RabbitmqConfig.BUSINESS_EXCHANGE,RabbitmqConfig.BUSINESS_ROUTING_KEY,"sendmessage");TimeUnit.SECONDS.sleep(10000);}}
2.2.2 创建消费者
@Component@RabbitListener(queues=RabbitmqConfig.BUSINESS_QUEUE_NAME)@Slf4jpublicclassBusinessConsumer{/***操作场景:*1.通过RabbitmqApplication启动类启动应用程序*2.调用/sendBusinessMessage接口发送消息*3.RabbitMQbroker将消息发送给消费者*4.消费者收到消息后进行消费*5.消费者消费消息过程中,应用程序关闭,断开channel,断开connection,未ack的消息会被重新放入broker中**@paramcontent消息内容*@paramchannelchannel通道*@parammessagemessage对象*/@RabbitHandlerpublicvoidhelloConsumer(Stringcontent,Channelchannel,Messagemessage){log.info("businessconsumerreceivemessage:{}",content);try{//模拟业务执行耗时TimeUnit.SECONDS.sleep(10000);}catch(InterruptedExceptione){e.printStackTrace();}}}
2.2.3 调用请求
http://localhost:8080/sendBusinessMessage
2.2.4 未关闭应用前
2.2.5 关闭应用后
2.2.6 结论
消息消费过程中,关闭应用,未ack的消息会被重新放入消息队列中,以此来保证消息一定会被消费
2.3 异步任务
2.3.1 线程池配置
@ComponentpublicclassThreadPoolConfig{@BeanpublicThreadPoolTaskExecutorthreadPoolTaskExecutor(){ThreadPoolTaskExecutorthreadPoolTaskExecutor=newThreadPoolTaskExecutor();threadPoolTaskExecutor.setThreadNamePrefix("test-");threadPoolTaskExecutor.setCorePoolSize(3);threadPoolTaskExecutor.setMaxPoolSize(3);threadPoolTaskExecutor.setQueueCapacity(100);returnthreadPoolTaskExecutor;}}
2.3.2 异步任务请求
@AutowiredprivateThreadPoolTaskExecutorthreadPoolTaskExecutor;@RequestMapping("async/task")publicvoidasyncTask()throwsInterruptedException{for(inti=0;i<10;i++){threadPoolTaskExecutor.execute(()->{try{TimeUnit.SECONDS.sleep(10);}catch(InterruptedExceptione){thrownewRuntimeException();}log.info("taskexecutecomplete...");});}}
2.3.3 调用请求
http://localhost:8080/async/task
2.3.4 模拟重启
kill-2应用pid
2.3.5 现象
Exceptioninthread"test-2"Exceptioninthread"test-1"Exceptioninthread"test-3"java.lang.RuntimeExceptionatcom.boot.example.ShutDownController.lambda$asyncTask$0(ShutDownController.java:37)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)atjava.lang.Thread.run(Thread.java:748)java.lang.RuntimeExceptionatcom.boot.example.ShutDownController.lambda$asyncTask$0(ShutDownController.java:37)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)atjava.lang.Thread.run(Thread.java:748)java.lang.RuntimeExceptionatcom.boot.example.ShutDownController.lambda$asyncTask$0(ShutDownController.java:37)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)atjava.lang.Thread.run(Thread.java:748)
2.3.6 修改线程池配置
在线程池配置中添加如下配置:
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);threadPoolTaskExecutor.setAwaitTerminationSeconds(120);
2.3.7 修改配置后现象
kill-2应用pid0
2.3.8 结论
使用线程池执行异步任务,在没有添加配置的情况下,任务无法执行完成,在添加配置的情况下,任务依然可以执行完成。
3. 总结
为了保证在应用程序重启过程中任务仍然可以执行完成,需要开启优雅关机
配置并对线程池添加等待任务执行完成
以及等待时间
配置