目錄
2、@EnableScheduling + @Scheduled 開啓定時任務
(2)使用shedlock將spring schedule上鎖
一、創建SpringBoot定時任務
此文章在SpringBoot框架的基礎上,創建SpringBoot定時任務,對於搭建SpringBoot框架可以閱讀我的相關文章,也可以直接到Spring官網下載相關代碼,在此不做贅述,直接上定時任務相關代碼。
定時任務類:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
// 每隔5s執行一次
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("The time is now {}", dateFormat.format(new Date()));
}
}
SpringBoot啓動類
@SpringBootApplication
@EnableScheduling
public class MyApplication {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(MyApplication.class);
try {
SpringApplication.run(MyApplication.class);
logger.info("springBoot啓動成功...");
} catch (Exception e) {
logger.info("SpringBoot啓動失敗...");
}
}
}
以上代碼是從官網Copy來的,是不是發現創建一個定時任務簡直不要太簡單?其實總的來說就兩個註解而已:@EnableScheduling、@Scheduled
下邊來剖析下SpringBoot定時任務的相關的一些註解
1、@SpringBootApplication啓動
之前用戶使用的是3個註解註解他們的main類。分別是@Configuration,@EnableAutoConfiguration,@ComponentScan。由於這些註解一般都是一起使用,springboot提供了一個統一的註解@SpringBootApplication。
@SpringBootApplication = (默認屬性)@Configuration + @EnableAutoConfiguration + @ComponentScan。
(1)@Configuration
提到@Configuration就要提到他的搭檔@Bean。使用這兩個註解就可以創建一個簡單的spring配置類,可以用來替代相應的xml配置文件。
<beans>
<bean id = "car" class="com.test.Car">
<property name="wheel" ref = "wheel"></property>
</bean>
<bean id = "wheel" class="com.test.Wheel"></bean>
</beans>
相當於
@Configuration
public class Conf {
@Bean
public Car car() {
Car car = new Car();
car.setWheel(wheel());
return car;
}
@Bean
public Wheel wheel() {
return new Wheel();
}
}
@Configuration的註解類標識這個類可以使用Spring IOC容器作爲bean定義的來源。@Bean註解告訴Spring,一個帶有@Bean的註解方法將返回一個對象,該對象應該被註冊爲在Spring應用程序上下文中的bean。
(2)@EnableAutoConfiguration
能夠自動配置spring的上下文,試圖猜測和配置你想要的bean類,通常會自動根據你的類路徑和你的bean定義自動配置。
(3)@ComponentScan
會自動掃描指定包下的全部標有@Component的類,並註冊成bean,當然包括@Component下的子註解@Service,@Repository,@Controller。
2、@EnableScheduling + @Scheduled 開啓定時任務
(1)@EnableScheduling
@EnableScheduling 在配置類上使用,開啓計劃任務的支持,沒有它,什麼都無法安排。
注:這個註解無論放在哪個類上,只要能檢索到@Scheduled,就能開啓定時任務。
註解源碼:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}
SchedulingConfiguration.class
類實現了Spring
的任務調度框架級功能。該配置類僅僅是定義了ScheduledAnnotationBeanPostProcessor
的實例。
其中SchedulingConfiguration.class
源碼:
@Configuration
@Role(2)
public class SchedulingConfiguration {
public SchedulingConfiguration() {
}
@Bean(
name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
)
@Role(2)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
(2)@Scheduled
@Scheduled 用來在方法上申明這是一個計劃任務,包括cron,fixDelay,fixRate等類型(需先開啓計劃任務的支持)。
使用fixedRate屬性每隔固定時間執行,使用cron屬性可按照指定時間執行(cron是UNIX和類UNIX(Linux)系統下的定時任務)。
源碼如下:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = "-";
String cron() default "";
String zone() default "";
long fixedDelay() default -1L;
String fixedDelayString() default "";
long fixedRate() default -1L;
String fixedRateString() default "";
long initialDelay() default -1L;
String initialDelayString() default "";
}
- cron 是設置定時執行的表達式,如 0 0/5 * * * ?每隔五分鐘執行一次
- zone 表示執行時間的時區,如"GMT+8",GMT是世界標準時間(格林威治時間),"GMT+8"表示東八區,即北京時間。
- fixedDelay 和 fixedDelayString 一個固定延遲時間執行,上個任務完成後,延遲多久執行
- fixedRate 和 fixedRateString 一個固定頻率執行,上個任務開始後多長時間後開始執行
- initialDelay 和 initialDelayString 表示一個初始延遲時間,第一次被調用前延遲的時間
(3)cron表達式
秒、分、時、日、月、周、年
cron表達式,有專門的語法,而且感覺有點繞人,不過簡單來說,大家記住一些常用的用法即可,特殊的語法可以單獨去查。
cron一共有7位,但是最後一位是年,可以留空,所以我們可以寫6位。
* 第一位,表示秒,取值0-59
* 第二位,表示分,取值0-59
* 第三位,表示小時,取值0-23
* 第四位,日期天/日,取值1-31
* 第五位,日期月份,取值1-12
* 第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二週的意思
另外:1表示星期天,2表示星期一。
* 第7爲,年份,可以留空,取值1970-2099
cron中,還有一些特殊的符號,含義如下:
(*)星號:可以理解爲每的意思,每秒,每分,每天,每月,每年...
(?)問號:問號只能出現在日期和星期這兩個位置。
(-)減號:表達一個範圍,如在小時字段中使用“10-12”,則表示從10到12點,即10,11,12
(,)逗號:表達一個列表值,如在星期字段中使用“1,2,4”,則表示星期一,星期二,星期四
(/)斜槓:如:x/y,x是開始值,y是步長,比如在第一位(秒) 0/15就是,從0秒開始,每15秒,最後就是0,15,30,45,60 另:*/y,等同於0/y
corn表達式示例:
0 0 3 * * ? 每天3點執行
0 5 3 * * ? 每天3點5分執行
0 5 3 ? * * 每天3點5分執行,與上面作用相同
0 5/10 3 * * ? 每天3點的 5分,15分,25分,35分,45分,55分這幾個時間點執行
0 10 3 ? * 1 每週星期天,3點10分 執行,注:1表示星期天
0 10 3 ? * 1#3 每個月的第三個星期,星期天 執行,#號只能出現在星期的位置
二、常見問題
1、單線程任務丟失,轉爲異步線程池
默認的 ConcurrentTaskScheduler 計劃執行器採用Executors.newSingleThreadScheduledExecutor() 實現單線程的執行器。因此,對同一個調度任務的執行總是同一個線程。如果任務的執行時間超過該任務的下一次執行時間,則會出現任務丟失,跳過該段時間的任務。上述問題有以下解決辦法:
採用異步的方式執行調度任務,配置 Spring 的 @EnableAsync,在執行定時任務的方法上標註 @Async 配置任務執行池,線程池大小 n 的數量爲:單個任務執行所需時間 / 任務執行的間隔時間。如下:
//每30秒執行一次
@Async("taskExecutor")
@Scheduled(fixedRate = 1000 * 3)
public void reportCurrentTime(){
System.out.println ("線程" + Thread.currentThread().getName() + "開始執行定時任務===&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&7&&&====》"
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
long start = System.currentTimeMillis();
Future<Boolean> isOk1;
Future<Boolean> isOk2;
...省略...
2、關於分佈式情況下,重複執行的問題(兩種方案)
(1)使用redis分佈式鎖
可以使用redis的分佈式鎖保證spring schedule集羣只執行一次。 redis分佈式鎖是通過setnx命令實現的。該命令的作用是,當往redis中存入一個值時,會先判斷該值對應的key是否存在,如果存在則返回0,如果不存在,則將該值存入redis並返回1。(但是在分佈式跨時區部署的時候,依然無法避免重複執行)
@Component
@Configuration
@EnableScheduling
public class AutoConvertTask {
private static final Logger logger = LoggerFactory.getLogger(AutoConvertTask.class);
@Autowired
private RedisTemplate redisTemplate;
private static final String LOCK = "task-job-lock";
private static final String KEY = "tasklock";
@Scheduled(cron = "0 0 0 * * ? ")
public void autoConvertJob() {
boolean lock = false;
try {
lock = redisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
logger.info("是否獲取到鎖:" + lock);
if (lock) {
List<GameHistory> historyList = historyService.findTenDaysAgoUntreated();
for (GameHistory history : historyList) {
update(history);
}
} else {
logger.info("沒有獲取到鎖,不執行任務!");
return;
}
} finally {
if (lock) {
redisTemplate.delete(KEY);
logger.info("任務結束,釋放鎖!");
} else {
logger.info("沒有獲取到鎖,無需釋放鎖!");
}
}
}
}
(2)使用shedlock將spring schedule上鎖
可以通過使用shedlock將spring schedule上鎖。詳細見:https://segmentfault.com/a/1190000011975027
3、服務器宕機之後,丟失的任務如何補償?
可以將每次的任務執行時間緩在redis裏,下次執行任務的時候都取出該時間,判斷是否爲上一個週期,如果不是,可以計算出中間丟失的週期數,然後做響應的補償操作。如果怕redis宕機,可以將“執行時間”持久化到表中。