定時任務的實現方式有多種,例如JDK自帶的Timer+TimerTask方式,Spring 3.0以後的調度任務(Scheduled Task),Quartz等。
因爲項目中用到了Scheduled,所以這裏只說Scheduled。
1. SpringBoot啓動類上加註解
@EnableScheduling
2. 自定義線程池。
spring底層默認是new一個核心數量爲1的單線程池,如果需要對定時器線程池核心線程數量調優或自定義什麼的,可以新增一個配置類,實現SchedulingConfigurer接口,重寫configureTasks方法,通過taskRegistrar設置自定義線程池。
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(20);
}
}
3. 用法:實現一個基本的調度方法。基本結構如下:
package com.netease.yx.service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ScheduledService {
@Scheduled(cron = "0 0 5 * * *")
public void build() {
System.out.println("Scheduled Task");
}
}
@Scheduled註解支持秒級的cron表達式,上述聲明表示每天5點執行build任務,當然本篇重點不是介紹cron,想知道更多的cron表達式,可以看我的另一篇博文 @Scheduled註解 詳解,裏面有詳細語法介紹。
好,回到正題,前文已經提過,這種方式在單臺應用服務器上運行沒有問題,但是在集羣環境下,會造成build任務在5點的時候運行多次,遺憾的是,Scheduled Task在框架層面沒有相應的解決方案,只能靠程序員在應用級別進行控制。
如何控制?
1. 無非是一個任務互斥訪問的問題,聲明一把全局的“鎖”作爲互斥量,哪個應用服務器拿到這把“鎖”,就有執行任務的權利,未拿到“鎖”的應用服務器不進行任何任務相關的操作。
2.這把“鎖”最好還能在下次任務執行時間點前失效。
在項目中我將這個互斥量放在了redis緩存裏,9分鐘過期,這個過期時間是由任務調度的間隔時間決定的,只要小於兩次任務執行時間差,大於集羣間應用服務器的時間差即可。
完整定時任務類如下,該段代碼支持多臺機子部署,不會出現多臺服務都同時執行的情況,當然前提是他們用的redis都是同一個:
@Component
public class AutoInsertVuserToGroupBuying {
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0 */10 * * * ?") //定時器10分鐘一次
public void shTask() {
//先判斷redis中是否有鎖記錄,如果能設值成功,代表拿到鎖,不能設值成功就是鎖還沒釋放
if(redisTemplate.opsForValue().setIfAbsent(key, value)){
//設值成功後,設置鎖超時時間 (我這裏是9分鐘)
redisTemplate.expire(key, 9, TimeUnit.MINUTES);
//業務
dojob();
}
}
}
PS: 很多人說9到11行之間會存在併發問題,其實並不會,因爲setIfAbsent先天自帶鎖,是基於redis的setnx來保證原子性,像這裏是不可能存在兩個線程同時設值成功的, 只有設值成功的才能拿到鎖,沒設值成功的就拿不到鎖.