<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          @Schedule定時任務+分布式環(huán)境,這些坑你一定得注意?。?!

          共 24854字,需瀏覽 50分鐘

           ·

          2024-08-02 09:19

          往期熱門文章:

          
              

          1、Spring純注解開發(fā),有點強!!

          2、放棄Java8的Stream流,我選擇使用JDFrame!

          3、SQLite這么小眾的數據庫,到底是什么人在用?

          4、微服務Token鑒權設計的幾種方案

          5、一天干了多少活兒,摸了多少魚,這個工具一目了然給你統(tǒng)計出來


          來源:juejin.cn/post/7155872110252916766

          定時任務的實現方式多種多樣,框架也是層出不窮。
          本文所談及的是 SpringBoot 本身所帶有的@EnableScheduling 、 @Scheduled實現定時任務的方式。
          以及采用這種方式,在分布式調度中可能會出現的問題,又針對為什么會發(fā)生這種問題?又該如何解決,做出了一些敘述。
          為了適合每個階段的讀者,我把前面測試的代碼都貼出來啦~
          確保每一步都是有跡可循的,希望大家不要嫌啰嗦,感謝
          一、搭建基本環(huán)境
          基本依賴
             
           <parent>
               <artifactId>spring-boot-parent</artifactId>
               <groupId>org.springframework.boot</groupId>
               <version>2.7.2</version>
           </parent>
           <dependencies>
               <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot</artifactId>
               </dependency>
               <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter</artifactId>
               </dependency>
               <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-web</artifactId>
               </dependency>
               <dependency>
                   <dependency>
                       <groupId>org.projectlombok</groupId>
                       <artifactId>lombok</artifactId>
                   </dependency>
            </dependencies>
          創(chuàng)建個啟動類及定時任務
             
          @SpringBootApplication
          public class ApplicationScheduling {

              public static void main(String[] args) {
                  SpringApplication.run(ApplicationScheduling.classargs);
              }
          }
             
          /**
           * @description:
           * @author: Ning Zaichun
           */

          @Slf4j
          @Component
          @EnableScheduling
          public class ScheduleService {

              // 每五秒執(zhí)行一次,cron的表達式就不再多說明了
              @Scheduled(cron = "0/5 * * * * ? ")
              public void testSchedule() {
                      log.info("當前執(zhí)行任務的線程號ID===>{}", Thread.currentThread().getId());
              }
          }


          二、問題::執(zhí)行時間延遲和單線程執(zhí)行


          按照上面代碼中給定的cron表達式@Scheduled(cron = "0/5 * * * * ? ")每五秒執(zhí)行一次,那么最近五次的執(zhí)行結果應當為:

             
           2022-09-06 00:21:10
           2022-09-06 00:21:15
           2022-09-06 00:21:20
           2022-09-06 00:21:25
           2022-09-06 00:21:30
          如果定時任務中是執(zhí)行非??斓娜蝿盏?,時間非常非常短,確實不會有什么的延遲性。
          上面代碼執(zhí)行結果:
             
           2022-09-06 19:42:10.018  INFO 24496 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:42:15.015  INFO 24496 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:42:20.001  INFO 24496 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:42:25.005  INFO 24496 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:42:30.007  INFO 24496 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
          如果說從時間上來看,說不上什么延遲性,但真實的業(yè)務場景中,業(yè)務的執(zhí)行時間可能遠比這里時間長。
          我主動讓線程睡上10秒,讓我們再來看看輸出結果是如何的吧
             
          @Scheduled(cron = "0/5 * * * * ? ")
          public void testSchedule() {
              try {
                  Thread.sleep(10000);
                  log.info("當前執(zhí)行任務的線程號ID===>{}", Thread.currentThread().getId());
              } catch (Exception e) {
                  e.printStackTrace();
              } 
          }
          輸出結果
             
           2022-09-06 19:46:50.019  INFO 27236 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:47:05.024  INFO 27236 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:47:20.016  INFO 27236 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:47:35.005  INFO 27236 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 19:47:50.006  INFO 27236 --- [   scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
          請注意兩個問題:
          • 執(zhí)行時間延遲: 從時間上可以明顯看出,不再是每五秒執(zhí)行一次,執(zhí)行時間延遲很多,造成任務的
          • 單線程執(zhí)行: 從始至終都只有一個線程在執(zhí)行任務,造成任務的堵塞.


          三、為什么會出現上述問題?


          問題的根本:線程阻塞式執(zhí)行,執(zhí)行任務線程數量過少。


          那到底是為什么呢?


          回到啟動類上,我們在啟動上標明了一個@EnableScheduling注解。


          大家在看到諸如@Enablexxxx這樣的注解的時候,就要知道它一定有一個xxxxxAutoConfiguration的自動裝配的類。


          @EnableScheduling也不例外,它的自動裝配的類是TaskSchedulingAutoConfiguration。


          我們來看看它到底做了一些什么設置?我們如何修改?

             
          @ConditionalOnClass(ThreadPoolTaskScheduler.class)
          @Configuration(proxyBeanMethods 
          false)
          @EnableConfigurationProperties(TaskSchedulingProperties.class)
          @AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
          public class TaskSchedulingAutoConfiguration 
          {

              @Bean
              @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
              @ConditionalOnMissingBean({ SchedulingConfigurer.classTaskScheduler.classScheduledExecutorService.class })
              public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder
          {
                  return builder.build();
              }
              
             // ......
          }
          可以看到它也是構造了一個 線程池注入到Spring 中
          從build()調用繼續(xù)看下去,
             
           public ThreadPoolTaskScheduler build() {
               return configure(new ThreadPoolTaskScheduler());
           }
          ThreadPoolTaskScheduler中,給定的線程池的核心參數就為1,這也表明了之前為什么只有一條線程在執(zhí)行任務。private volatile int poolSize = 1;
          這一段是分開的用代碼不好展示,我用圖片標明出來。
          主要邏輯在這里,創(chuàng)建線程池的時候,只使用了三個參數,剩下的都是使用ScheduledExecutorService的默認的參數
             
          protected ScheduledExecutorService createExecutor(
                  int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) 
          而這默認參數是不行的,生產環(huán)境的大坑,阿里的 Java 開發(fā)手冊中也明確規(guī)定,要手動創(chuàng)建線程池,并給定合適的參數值~是為什么呢?
          因為默認的線程池中, 池中允許的最大線程數和最大任務等待隊列都是Integer.MAX_VALUE.
          大家都懂的,如果使用這玩意,只要出了問題,必定掛~
          configure(new ThreadPoolTaskScheduler())這里就是構造,略過~
          如果已經較為熟悉SpringBoot的朋友,現在已然明白解決當前問題的方式~

          四、解決方式


          1、@EnableConfigurationProperties(TaskSchedulingProperties.class) ,自動裝配類通常也都會對應有個xxxxProperties文件滴,TaskSchedulingProperties也確實可以配置核心線程數等基本參數,但是無法配置線程池中最大的線程數量和等待隊列數量,這種方式還是不合適的。


          2、可以手動異步編排,交給某個線程池來執(zhí)行。
          3、將定時任務加上異步注解@Async,將其改為異步的定時任務,另外自定義一個系統(tǒng)通用的線程池,讓異步任務使用該線程執(zhí)行任務~
          我們分別針對上述三種方式來實現一遍

          4.1、修改配置文件


          可以配置的就下面幾項~

             
           spring:
             task:
               scheduling:
                 thread-name-prefix: nzc-schedule- #線程名前綴
                 pool:
                   size: 10 #核心線程數
                # shutdown:
                #  await-termination: true #執(zhí)行程序是否應等待計劃任務在關機時完成。
                #   await-termination-period:  #執(zhí)行程序應等待剩余任務完成的最長時間。
          測試結果:
             
           2022-09-06 20:49:15.015  INFO 7852 --- [ nzc-schedule-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 20:49:30.004  INFO 7852 --- [ nzc-schedule-2] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>66
           2022-09-06 20:49:45.024  INFO 7852 --- [ nzc-schedule-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>64
           2022-09-06 20:50:00.025  INFO 7852 --- [ nzc-schedule-3] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>67
           2022-09-06 20:50:15.023  INFO 7852 --- [ nzc-schedule-2] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>66
           2022-09-06 20:50:30.008  INFO 7852 --- [ nzc-schedule-4] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>68
          請注意:這里的配置并非是一定生效的,修改后有可能成功,有可能失敗,具體原因未知,但這一點是真實存在的。
          不過從執(zhí)行結果中可以看出,這里的執(zhí)行的線程不再是孤單單的一個。

          4.2、執(zhí)行邏輯改為異步執(zhí)行


          首先我們先向Spring中注入一個我們自己編寫的線程池,參數自己設置即可,我這里比較隨意。

             
           @Configuration
           public class MyTheadPoolConfig {
           
               @Bean
               public TaskExecutor taskExecutor() {
                   ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
                   //設置核心線程數
                   executor.setCorePoolSize(10);
                   //設置最大線程數
                   executor.setMaxPoolSize(20);
                   //緩沖隊列200:用來緩沖執(zhí)行任務的隊列
                   executor.setQueueCapacity(200);
                   //線程活路時間 60 秒
                   executor.setKeepAliveSeconds(60);
                   //線程池名的前綴:設置好了之后可以方便我們定位處理任務所在的線程池
                   // 這里我繼續(xù)沿用 scheduling 默認的線程名前綴
                   executor.setThreadNamePrefix("nzc-create-scheduling-");
                   //設置拒絕策略
                   executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
                   executor.setWaitForTasksToCompleteOnShutdown(true);
                   return executor;
               }
           
           }
          然后在定時任務這里注入進去:
             
           /**
            * @description:
            * @author: Ning Zaichun
            */

           @Slf4j
           @Component
           @EnableScheduling
           public class ScheduleService {
           
               @Autowired
               TaskExecutor taskExecutor;
           
               @Scheduled(cron = "0/5 * * * * ? ")
               public void testSchedule() {
                   CompletableFuture.runAsync(()->{
                       try {
                           Thread.sleep(10000);
                           log.info("當前執(zhí)行任務的線程號ID===>{}", Thread.currentThread().getId());
                       } catch (Exception e) {
                           e.printStackTrace();
                       } 
                   },taskExecutor);
               }
           }
          測試結果:
             
           2022-09-06 21:00:00.019  INFO 18356 --- [te-scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>66
           2022-09-06 21:00:05.022  INFO 18356 --- [te-scheduling-2] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>67
           2022-09-06 21:00:10.013  INFO 18356 --- [te-scheduling-3] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>68
           2022-09-06 21:00:15.020  INFO 18356 --- [te-scheduling-4] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>69
           2022-09-06 21:00:20.026  INFO 18356 --- [te-scheduling-5] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>70
          可以看到雖然業(yè)務執(zhí)行時間比較長,但是木有再出現,延遲執(zhí)行定時任務的情況。
          4.3、異步定時任務
          異步定時任務其實和上面的方式原理是一樣的,不過實現稍稍不同罷了。
          在定時任務的類上再加一個@EnableAsync注解,給方法添加一個@Async即可。
          不過一般@Async都會指定線程池,比如寫成這樣@Async(value = "taskExecutor"),
             
           /**
            * @description:
            * @author: Ning Zaichun
            */

           @Slf4j
           @Component
           @EnableAsync
           @EnableScheduling
           public class ScheduleService {
           
               @Autowired
               TaskExecutor taskExecutor;
           
               @Async(value = "taskExecutor")
               @Scheduled(cron = "0/5 * * * * ? ")
               public void testSchedule() {
                       try {
                           Thread.sleep(10000);
                           log.info("當前執(zhí)行任務的線程號ID===>{}", Thread.currentThread().getId());
                       } catch (Exception e) {
                           e.printStackTrace();
                       } 
               }
           }
          執(zhí)行結果:
             
           2022-09-06 21:10:15.022  INFO 22760 --- [zc-scheduling-1] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>66
           2022-09-06 21:10:20.021  INFO 22760 --- [zc-scheduling-2] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>67
           2022-09-06 21:10:25.007  INFO 22760 --- [zc-scheduling-3] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>68
           2022-09-06 21:10:30.020  INFO 22760 --- [zc-scheduling-4] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>69
           2022-09-06 21:10:35.007  INFO 22760 --- [zc-scheduling-5] com.nzc.service.ScheduleService          : 當前執(zhí)行任務的線程號ID===>70
          結果顯而易見是可行的啦~
          分析:
          @EnableAsync注解相應的也有一個自動裝配類為TaskExecutionAutoConfiguration
          也有一個TaskExecutionProperties配置類,可以在yml文件中對參數進行設置,這里的話是可以配置線程池最大存活數量的。
          它的默認核心線程數為8,這里我不再進行演示了,同時它的線程池中最大存活數量以及任務等待數量也都為Integer.MAX_VALUE,這也是不建議大家使用默認線程池的原因。

          4.4、小結

             
           /**
            * 定時任務
            *      1、@EnableScheduling 開啟定時任務
            *      2、@Scheduled開啟一個定時任務
            *      3、自動裝配類 TaskSchedulingAutoConfiguration
            *
            * 異步任務
            *      1、@EnableAsync:開啟異步任務
            *      2、@Async:給希望異步執(zhí)行的方法標注
            *      3、自動裝配類 TaskExecutionAutoConfiguration
            */
          實現方式雖不同,但從效率而言,并無太大區(qū)別,覺得那種合適使用那種便可。
          不過總結起來,考查的都是對線程池的理解,對于線程池的了解是真的非常重要的,也很有用處。
          五、分布式下的思考
          針對上述情況而言,這些解決方法在不引入第三包的情況下是足以應付大部分情況了。
          定時框架的實現有許多方式,在此并非打算討論這個。
          在單體項目中,也許上面的問題是解決了,但是站在分布式的情況下考慮,就并非是安全的了。
          當多個項目在同時運行,那么必然會有多個項目同時這段代碼。
          思考:并發(fā)執(zhí)行
          如果一個定時任務同時在多個機器中運行,會產生怎么樣的問題?
          假如這個定時任務是收集某個信息,發(fā)送給消息隊列,如果多臺機器同時執(zhí)行,同時給消息隊列發(fā)送信息,那么必然導致之后產生一系列的臟數據。這是非常不可靠的


          解決方式:分布式鎖


          很簡單也不簡單,加分布式鎖~ 或者是用一些分布式調度的框架


          如使用XXL-JOB實現,或者是其他的定時任務框架。


          大家在執(zhí)行這個定時任務之前,先去獲取一把分布式鎖,獲取到了就執(zhí)行,獲取不到就直接結束。


          我這里使用的是 redission,因為方便,打算寫分布式鎖的文章,還在準備當中。


          加入依賴:

             
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-data-redis</artifactId>
           </dependency>
           <dependency>
               <groupId>org.redisson</groupId>
               <artifactId>redisson-spring-boot-starter</artifactId>
               <version>3.17.6</version>
           </dependency>
          按照文檔說的,編寫配置類,注入 RedissonClient,redisson的全部操作都是基于此。
             
           /**
            * @description:
            * @author: Ning Zaichun
            */

           @Configuration
           public class MyRedissonConfig {
           
               /**
                * 所有對Redisson的使用都是通過RedissonClient
                * @return
                * @throws IOException
                */

               @Bean(destroyMethod="shutdown")
               public RedissonClient redissonClient() throws IOException {
                   //1、創(chuàng)建配置
                   Config config = new Config();
                  // 這里規(guī)定要用 redis://+IP地址
                     config.useSingleServer().setAddress("redis://xxxxx:6379").setPassword("000415");   // 有密碼就寫密碼~ 木有不用寫~
                   //2、根據Config創(chuàng)建出RedissonClient實例
                   //Redis url should start with redis:// or rediss://
                   RedissonClient redissonClient = Redisson.create(config);
                   return redissonClient;
               }
           }
          修改定時任務:
             
           /**
            * @description:
            * @author: Ning Zaichun
            */

           @Slf4j
           @Component
           @EnableAsync
           @EnableScheduling
           public class ScheduleService {
           
               @Autowired
               TaskExecutor taskExecutor;
           
               @Autowired
               RedissonClient redissonClient;
           
               private final String SCHEDULE_LOCK = "schedule:lock";
           
               @Async(value = "taskExecutor")
               @Scheduled(cron = "0/5 * * * * ? ")
               public void testSchedule() {
                   //分布式鎖
                   RLock lock = redissonClient.getLock(SCHEDULE_LOCK);
                   try {
                       //加鎖 10 為時間,加上時間 默認會去掉 redisson 的看門狗機制(即自動續(xù)鎖機制)
                       lock.lock(10, TimeUnit.SECONDS);
                       Thread.sleep(10000);
                       log.info("當前執(zhí)行任務的線程號ID===>{}", Thread.currentThread().getId());
                   } catch (Exception e) {
                       e.printStackTrace();
                   } finally {
                       // 一定要記得解鎖~
                       lock.unlock();
                   }
               }
           }
          這里只是給出個大概的實現,實際上還是可以優(yōu)化的,比如在給定一個flag,在獲取鎖之前判斷。如果有人搶到鎖,就修改這個值,之后的請求,判斷這個flag,如果不是默認的值,則直接結束任務等等。
          思考:繼續(xù)往深處思考,在分布式情況下如果一個定時任務搶到鎖,但是它在執(zhí)行業(yè)務過程中失敗或者是宕機了,這又該如何處理呢?如何補償呢?
          個人思考:
          失敗還比較好說,我們可以直接try{}catch(){}中進行通知告警,及時檢查出問題。
          如果是掛了,我還沒想好怎么做。
          后記
          但實際上,我所闡述的這種方式,只能說適用于簡單的單體項目,一旦牽扯到動態(tài)定時任務,使用這種方式就不再那么方便了。
          大部分都是使用定時任務框架集成了,尤其是分布式調度遠比單體項目需要考慮多的多。
             
          往期熱門文章:

          1、編程語言座次圖,誰才是老大?(PS:原來這么多編程語言都有同一個祖宗)
          2、如果網站的 Cookie 超過 4K,會發(fā)生什么情況?
          3、如何優(yōu)雅的實現在線人數統(tǒng)計功能?
          4、JetBrains再出手,這波秀翻了。。
          5、FullGC 40次/天到10天1次,真牛B?。?/a>
          6、不服不行,這才是后端API接口應該有的樣子!
          7、一個強大的分布式鎖框架——Lock4j
          8、Stream很好,Map很酷,但答應我別用toMap()
          9、你合并代碼用 merge 還是用 rebase ?
          10、99%的時間里只使用這14個Git命令就夠了?。?!


          瀏覽 266
          1點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          1點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产高清无码黄片 | 亚洲色图欧美视频 | 免费一级a毛片在线播放直播 | 丁香花五月激 | 日韩资源在线观看 |