快速上手SpringBoot定時任務

目錄

一、創建SpringBoot定時任務

1、@SpringBootApplication啓動

(1)@Configuration

(2)@EnableAutoConfiguration

(3)@ComponentScan

2、@EnableScheduling + @Scheduled 開啓定時任務

(1)@EnableScheduling

(2)@Scheduled

(3)cron表達式

二、常見問題

1、單線程任務丟失,轉爲異步線程池

2、關於分佈式情況下,重複執行的問題(兩種方案)

(1)使用redis分佈式鎖

(2)使用shedlock將spring schedule上鎖

3、服務器宕機之後,丟失的任務如何補償? 


一、創建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 用來在方法上申明這是一個計劃任務,包括cronfixDelayfixRate等類型(需先開啓計劃任務的支持)。

使用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 "";
}
  1. cron 是設置定時執行的表達式,如 0 0/5 * * * ?每隔五分鐘執行一次
  2. zone 表示執行時間的時區,如"GMT+8",GMT是世界標準時間(格林威治時間),"GMT+8"表示東八區,即北京時間。
  3. fixedDelay 和 fixedDelayString 一個固定延遲時間執行,上個任務完成後,延遲多久執行
  4. fixedRate 和 fixedRateString 一個固定頻率執行,上個任務開始後多長時間後開始執行
  5. 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宕機,可以將“執行時間”持久化到表中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章