Java之定時任務全家桶

挑戰者V發表於2020-11-22

定時任務應用非常廣泛,Java提供的現有解決方案有很多。
本次主要講schedule、quartz、xxl-job、shedlock等相關的程式碼實踐。

一、SpringBoot使用Schedule

核心程式碼:

@Component
public class ScheduleTask {

    private Logger logger = LoggerFactory.getLogger(ScheduleTask.class);

    @Scheduled(cron = "0/1 * * * * ? ")
    public void one() {

        logger.info("one:" + new Date());
    }

    @Scheduled(cron = "0/1 * * * * ? ")
    public void two() {

        logger.info("two:" + new Date());
    }


    @Scheduled(cron = "0/1 * * * * ? ")
    public void three() {

        logger.info("three:" + new Date());
    }
}

執行效果如下:
圖一

除此之外還可以這樣實現,核心程式碼:

@PropertySource(value = {
        "classpath:task.properties",
}, encoding = "utf-8")
@Component("scheduleTask")
public class ScheduleTask implements SchedulingConfigurer {


    @Value("${TEST_JOB_TASK_CRON}")
    private String cron;

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {

        scheduledTaskRegistrar.addTriggerTask(new Runnable() {

            @Override
            public void run() {
                System.out.println("執行任務:" + DateUtil.date());


            }

        }, new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                return new CronTrigger(cron).nextExecutionTime(triggerContext);
            }
        });
    }

    public void setCron(String cron) {
        this.cron = cron;
    }
}

有朋友或許很疑惑,為什麼要寫這麼一大堆,這個與前面的程式碼又有何區別呢?
區別是多執行緒並行。其實多執行緒並行也可以不用這麼寫,只需寫一段核心配置類程式碼即可。

定時任務多執行緒配置類:

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {

    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
    }
}

再次啟動,檢視效果,如下:
圖二
由此看出走不同的執行緒執行,不同的執行緒執行的好處是,如果某一個執行緒掛掉後,並不會阻塞導致其它定時任務無法執行。

另外如果要想併發執行,前面的配置可以不要,直接用SpringBoot提供的現成註解即可,核心程式碼如下:

@Component
@EnableAsync
public class ScheduleAsyncTask {

    private Logger logger = LoggerFactory.getLogger(ScheduleAsyncTask.class);

    @Scheduled(cron = "0/1 * * * * ? ")
    @Async
    public void one() {

        logger.info("one Async:" + new Date());
    }

    @Scheduled(cron = "0/1 * * * * ? ")
    @Async
    public void two() {

        logger.info("two Async:" + new Date());
    }


    @Scheduled(cron = "0/1 * * * * ? ")
    @Async
    public void three() {

        logger.info("three Async:" + new Date());
    }
}

除此外,還有基於schedule動態定時任務(所謂動態只不過是指cron表示式放在對應的資料表裡),簡單示例程式碼:

@Configuration
public class DynamicScheduleTask implements SchedulingConfigurer {

    @Autowired
    @SuppressWarnings("all")
    CronMapper cronMapper;

    @Mapper
    public interface CronMapper {
        @Select("select cron from cron limit 1")
        public String getCron();
    }

    /**
     * 執行定時任務.
     */
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.addTriggerTask(
                //1.新增任務內容(Runnable)
                () -> System.out.println("執行動態定時任務: " + LocalDateTime.now().toLocalTime()),
                //2.設定執行週期(Trigger)
                triggerContext -> {
                    //2.1 從資料庫獲取執行週期
                    String cron = cronMapper.getCron();
                    //2.2 合法性校驗.
                    if (StringUtils.isEmpty(cron)) {
                        // Omitted Code ..
                    }
                    //2.3 返回執行週期(Date)
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }



}

核心配置檔案(application.yml):

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: 1234

SQL指令碼:

DROP DATABASE IF EXISTS `test`;
CREATE DATABASE `test`;
USE `test`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron`  (
  `cron_id` varchar(30) NOT NULL PRIMARY KEY,
  `cron` varchar(30) NOT NULL  
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');

執行效果如下:

圖三

二、SpringBoot使用Quartz

1.Maven依賴

<!--引入quartz定時框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2.配置檔案

spring:
  quartz:
    #相關屬性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: clusteredScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 10000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
    #資料庫方式
    job-store-type: jdbc
    #初始化表結構
    jdbc:
      initialize-schema: always
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: 1234

3.啟動類

@SpringBootApplication
@EnableScheduling
public class BlogQuartzApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlogQuartzApplication.class, args);
    }


}

4.配置類

@Configuration
public class QuartzConfiguration {
    // 使用jobDetail包裝job
    @Bean
    public JobDetail myCronJobDetail() {
        return JobBuilder.newJob(CouponTimeOutJob.class).withIdentity("couponTimeOutJob").storeDurably().build();
    }

    // 把jobDetail註冊到Cron表示式的trigger上去
    @Bean
    public Trigger CronJobTrigger() {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");

        return TriggerBuilder.newTrigger()
                .forJob(myCronJobDetail())
                .withIdentity("CouponTimeOutJobTrigger")
                .withSchedule(cronScheduleBuilder)
                .build();
    }
}

5.定時任務類

public class CouponTimeOutJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("定時任務執行");
    }
}

6.啟動成功不報錯

(1)對應的資料庫會生成定時任務相關的資料表

圖四

(2)控制檯不斷輸出定時任務執行日誌

圖五

三、SpringBoot使用xxl-job

之前寫過一樣的例子,如今簡化了下。
關於xxl-job使用詳情,可以參考我的這篇文章:
SpringBoot整合Xxl-Job

1.Maven依賴

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.2.0</version>
</dependency>

2.配置類

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appName;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;


    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appName);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

}

3.配置檔案內容

# web port
server.port=8081
# no web
#spring.main.web-environment=false
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job, access token
xxl.job.accessToken=
### xxl-job executor appname
xxl.job.executor.appname=blog-job-xxl-job
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=8888
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30

4.定時任務類

@Component
public class XxlJobTaskExample {


    @XxlJob("blogJobHandler")
    public ReturnT<String> blogJobHandler(String param) throws Exception {
        System.out.println("執行");
        XxlJobLogger.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return ReturnT.SUCCESS;
    }

}

5.執行效果

分別如下所示:
圖六

圖七

四、SpringBoot使用ShedLock

1.匯入Maven依賴

<!-- 分散式定時任務鎖 -->
   <!-- https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring -->
   <dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-spring</artifactId>
       <version>4.0.4</version>
   </dependency>
   <!-- 使用redis做分散式任務 -->
   <dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-provider-redis-spring</artifactId>
       <version>2.5.0</version>
   </dependency>
   <!-- redis元件 -->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
   </dependency>

2.編寫配置類

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
public class ShedLockConfig {

    @Bean
    public LockProvider lockProvider(RedisTemplate redisTemplate) {
        return new RedisLockProvider(redisTemplate.getConnectionFactory());

    }

}

3.編寫具體的定時任務

@Component
public class TaskSchedule {

    /**
     * 每分鐘執行一次
     * [秒] [分] [小時] [日] [月] [周] [年]
     */
    @Scheduled(cron = "1 * * * * ?")
    @SchedulerLock(name = "synchronousSchedule")
    public void SynchronousSchedule() {
        
        System.out.println("Start run schedule to synchronous data:" + new Date());

    }
}

4.編寫啟動類

@SpringBootApplication
@EnableScheduling
public class ShedLockRedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShedLockRedisApplication.class);
    }
}

5.配置檔案

server:
  tomcat:
    uri-encoding: UTF-8
    max-threads: 1000
    min-spare-threads: 30
  port: 8083

spring:
  redis:
    database: 0
    host: localhost
    port: 6379
    password:      # 密碼(預設為空)
    timeout: 6000ms  # 連線超時時長(毫秒)
    jedis:
      pool:
        max-active: 1000  # 連線池最大連線數(使用負值表示沒有限制)
        max-wait: -1ms      # 連線池最大阻塞等待時間(使用負值表示沒有限制)
        max-idle: 10      # 連線池中的最大空閒連線
        min-idle: 5       # 連線池中的最小空閒連線

6.測試

我之所以用shedlock是因為確保在叢集環境下各微服務的定時任務只執行一個,而不是全部都執行相同的定時任務。

本次測試效果如下:
圖八

本次程式碼例子已放至我的GitHub:
https://github.com/developers-youcong/blog-job

相關文章