@Schedule定時任務+分布式環(huán)境,這些坑你一定得注意?。?!
共 24854字,需瀏覽 50分鐘
·
2024-08-02 09:19
往期熱門文章:
2、放棄Java8的Stream流,我選擇使用JDFrame!
5、一天干了多少活兒,摸了多少魚,這個工具一目了然給你統(tǒng)計出來
來源:juejin.cn/post/7155872110252916766
<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>
@SpringBootApplication
public class ApplicationScheduling {
public static void main(String[] args) {
SpringApplication.run(ApplicationScheduling.class, args);
}
}
/**
* @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
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
@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.class, TaskScheduler.class, ScheduledExecutorService.class })
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
// ......
}
public ThreadPoolTaskScheduler build() {
return configure(new ThreadPoolTaskScheduler());
}
protected ScheduledExecutorService createExecutor(
int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler)
四、解決方式
1、@EnableConfigurationProperties(TaskSchedulingProperties.class) ,自動裝配類通常也都會對應有個xxxxProperties文件滴,TaskSchedulingProperties也確實可以配置核心線程數等基本參數,但是無法配置線程池中最大的線程數量和等待隊列數量,這種方式還是不合適的。
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
請注意:這里的配置并非是一定生效的,修改后有可能成功,有可能失敗,具體原因未知,但這一點是真實存在的。
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
/**
* @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();
}
}
}
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
4.4、小結
/**
* 定時任務
* 1、@EnableScheduling 開啟定時任務
* 2、@Scheduled開啟一個定時任務
* 3、自動裝配類 TaskSchedulingAutoConfiguration
*
* 異步任務
* 1、@EnableAsync:開啟異步任務
* 2、@Async:給希望異步執(zhí)行的方法標注
* 3、自動裝配類 TaskExecutionAutoConfiguration
*/
在單體項目中,也許上面的問題是解決了,但是站在分布式的情況下考慮,就并非是安全的了。
假如這個定時任務是收集某個信息,發(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>
/**
* @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();
}
}
}
思考:繼續(xù)往深處思考,在分布式情況下如果一個定時任務搶到鎖,但是它在執(zhí)行業(yè)務過程中失敗或者是宕機了,這又該如何處理呢?如何補償呢?
往期熱門文章:
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命令就夠了?。?!
評論
圖片
表情
