第三十九章:基于SpringBoot & Quartz完成定時(shí)任務(wù)分布式單節(jié)點(diǎn)持久化

定時(shí)任務(wù)在企業(yè)項(xiàng)目比較常用到,幾乎所有的項(xiàng)目都會(huì)牽扯該功能模塊,定時(shí)任務(wù)一般會(huì)處理指定時(shí)間點(diǎn)執(zhí)行某一些業(yè)務(wù)邏輯、間隔時(shí)間執(zhí)行某一些業(yè)務(wù)邏輯等。我們?cè)谥坝兄v過SpringBoot是已經(jīng)集成了定時(shí)任務(wù)的,詳見:第二十六章:SpringBoot使用@Scheduled創(chuàng)建定時(shí)任務(wù),那么我們本章將會(huì)采用外置的quartz定時(shí)任務(wù)框架來完成定時(shí)任務(wù)的分布式單節(jié)點(diǎn)持久化,我們?yōu)槭裁匆志没〞r(shí)任務(wù)呢?

在一些項(xiàng)目中定時(shí)任務(wù)可能是必不可少的,由于某種特殊的原因定時(shí)任務(wù)可能丟失,如重啟定時(shí)任務(wù)服務(wù)項(xiàng)目后,原內(nèi)存中的定時(shí)任務(wù)就會(huì)被完全釋放!那對(duì)于我們來說可能是致命的問題。當(dāng)然也有強(qiáng)制的辦法解決這類問題,但是如果我們把定時(shí)任務(wù)持久化到數(shù)據(jù)庫,像維護(hù)普通邏輯數(shù)據(jù)那樣維護(hù)任務(wù),就會(huì)避免項(xiàng)目中遇到的種種的特殊情況。

免費(fèi)教程專題

恒宇少年在博客整理三套免費(fèi)學(xué)習(xí)教程專題,由于文章偏多特意添加了閱讀指南,新文章以及之前的文章都會(huì)在專題內(nèi)陸續(xù)填充,希望可以幫助大家解惑更多知識(shí)點(diǎn)。

本章目標(biāo)

基于SpringBoot架構(gòu)整合定時(shí)任務(wù)框架quartz來完成分布式單節(jié)點(diǎn)定時(shí)任務(wù)持久化,將任務(wù)持久化到數(shù)據(jù)庫,更好的預(yù)防任務(wù)丟失。

SpringBoot 企業(yè)級(jí)核心技術(shù)學(xué)習(xí)專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術(shù) 講解SpringBoot一些企業(yè)級(jí)層面的核心組件
002 Spring Boot 核心技術(shù)章節(jié)源碼 Spring Boot 核心技術(shù)簡(jiǎn)書每一篇文章碼云對(duì)應(yīng)源碼
003 Spring Cloud 核心技術(shù) 對(duì)Spring Cloud核心技術(shù)全面講解
004 Spring Cloud 核心技術(shù)章節(jié)源碼 Spring Cloud 核心技術(shù)簡(jiǎn)書每一篇文章對(duì)應(yīng)源碼
005 QueryDSL 核心技術(shù) 全面講解QueryDSL核心技術(shù)以及基于SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術(shù) 全面講解SpringDataJPA核心技術(shù)
007 SpringBoot核心技術(shù)學(xué)習(xí)目錄 SpringBoot系統(tǒng)的學(xué)習(xí)目錄,敬請(qǐng)關(guān)注點(diǎn)贊?。?

構(gòu)建項(xiàng)目

我們使用idea開發(fā)工具創(chuàng)建一個(gè)SpringBoot項(xiàng)目,pom.xml依賴配置如下所示:

...省略部分配置
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <druid.version>1.1.5</druid.version>
        <quartz.version>2.3.0</quartz.version>
    </properties>

    <dependencies>
        <!--spring data jpa相關(guān)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--web相關(guān)依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--數(shù)據(jù)庫相關(guān)依賴-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--quartz相關(guān)依賴-->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>${quartz.version}</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>${quartz.version}</version>
        </dependency>
        <!--定時(shí)任務(wù)需要依賴context模塊-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
...省略部分配置

我們采用的是quartz官方最新版本2.3.0,新版本的任務(wù)調(diào)度框架做出了很多封裝,使用也變得簡(jiǎn)易明了。
創(chuàng)建初始化完成,下面我們來創(chuàng)建定時(shí)任務(wù)相關(guān)的Configuration配置。

QuartzConfiguration

quartzSpring相關(guān)框架的整合方式有很多種,我們今天采用jobDetail使用Spring Ioc托管方式來完成整合,我們可以在定時(shí)任務(wù)實(shí)例中使用Spring注入注解完成業(yè)務(wù)邏輯處理,下面我先把全部的配置貼出來再逐步分析,配置類如下所示:

package com.hengyu.chapter39.configuration;

import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import javax.sql.DataSource;

/**
 * quartz定時(shí)任務(wù)配置
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/11/5
 * Time:14:07
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author  恒宇少年
 */
@Configuration
@EnableScheduling
public class QuartzConfiguration
{
    /**
     * 繼承org.springframework.scheduling.quartz.SpringBeanJobFactory
     * 實(shí)現(xiàn)任務(wù)實(shí)例化方式
     */
    public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
            ApplicationContextAware {

        private transient AutowireCapableBeanFactory beanFactory;

        @Override
        public void setApplicationContext(final ApplicationContext context) {
            beanFactory = context.getAutowireCapableBeanFactory();
        }

        /**
         * 將job實(shí)例交給spring ioc托管
         * 我們?cè)趈ob實(shí)例實(shí)現(xiàn)類內(nèi)可以直接使用spring注入的調(diào)用被spring ioc管理的實(shí)例
         * @param bundle
         * @return
         * @throws Exception
         */
        @Override
        protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            /**
             * 將job實(shí)例交付給spring ioc
             */
            beanFactory.autowireBean(job);
            return job;
        }
    }

    /**
     * 配置任務(wù)工廠實(shí)例
     * @param applicationContext spring上下文實(shí)例
     * @return
     */
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext)
    {
        /**
         * 采用自定義任務(wù)工廠 整合spring實(shí)例來完成構(gòu)建任務(wù)
         * see {@link AutowiringSpringBeanJobFactory}
         */
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    /**
     * 配置任務(wù)調(diào)度器
     * 使用項(xiàng)目數(shù)據(jù)源作為quartz數(shù)據(jù)源
     * @param jobFactory 自定義配置任務(wù)工廠
     * @param dataSource 數(shù)據(jù)源實(shí)例
     * @return
     * @throws Exception
     */
    @Bean(destroyMethod = "destroy",autowire = Autowire.NO)
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception
    {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        //將spring管理job自定義工廠交由調(diào)度器維護(hù)
        schedulerFactoryBean.setJobFactory(jobFactory);
        //設(shè)置覆蓋已存在的任務(wù)
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //項(xiàng)目啟動(dòng)完成后,等待2秒后開始執(zhí)行調(diào)度器初始化
        schedulerFactoryBean.setStartupDelay(2);
        //設(shè)置調(diào)度器自動(dòng)運(yùn)行
        schedulerFactoryBean.setAutoStartup(true);
        //設(shè)置數(shù)據(jù)源,使用與項(xiàng)目統(tǒng)一數(shù)據(jù)源
        schedulerFactoryBean.setDataSource(dataSource);
        //設(shè)置上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        //設(shè)置配置文件位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }
}

AutowiringSpringBeanJobFactory

可以看到上面配置類中,AutowiringSpringBeanJobFactory我們繼承了SpringBeanJobFactory類,并且通過實(shí)現(xiàn)ApplicationContextAware接口獲取ApplicationContext設(shè)置方法,通過外部實(shí)例化時(shí)設(shè)置ApplicationContext實(shí)例對(duì)象,在createJobInstance方法內(nèi),我們采用AutowireCapableBeanFactory來托管SpringBeanJobFactory類中createJobInstance方法返回的定時(shí)任務(wù)實(shí)例,這樣我們就可以在定時(shí)任務(wù)類內(nèi)使用Spring Ioc相關(guān)的注解進(jìn)行注入業(yè)務(wù)邏輯實(shí)例了。

JobFactory

任務(wù)工廠是在本章配置調(diào)度器時(shí)所需要的實(shí)例,我們通過jobFactory方法注入ApplicationContext實(shí)例,來創(chuàng)建一個(gè)AutowiringSpringBeanJobFactory對(duì)象,并且將對(duì)象實(shí)例托管到Spring Ioc容器內(nèi)。

SchedulerFactoryBean

我們本章采用的是項(xiàng)目內(nèi)部數(shù)據(jù)源的方式來設(shè)置調(diào)度器的jobSotre,官方quartz有兩種持久化的配置方案。

第一種:采用quartz.properties配置文件配置獨(dú)立的定時(shí)任務(wù)數(shù)據(jù)源,可以與使用項(xiàng)目的數(shù)據(jù)庫完全獨(dú)立。
第二種:采用與創(chuàng)建項(xiàng)目統(tǒng)一個(gè)數(shù)據(jù)源,定時(shí)任務(wù)持久化相關(guān)的表與業(yè)務(wù)邏輯在同一個(gè)數(shù)據(jù)庫內(nèi)。

可以根據(jù)實(shí)際的項(xiàng)目需求采取不同的方案,我們本章主要是通過第二種方案來進(jìn)行講解,在上面配置類中可以看到方法schedulerFactoryBean內(nèi)自動(dòng)注入了JobFactory實(shí)例,也就是我們自定義的AutowiringSpringBeanJobFactory任務(wù)工廠實(shí)例,另外一個(gè)參數(shù)就是DataSource,在我們引入spring-starter-data-jpa依賴后會(huì)根據(jù)application.yml文件內(nèi)的數(shù)據(jù)源相關(guān)配置自動(dòng)實(shí)例化DataSource實(shí)例,這里直接注入是沒有問題的。

我們通過調(diào)用SchedulerFactoryBean對(duì)象的setConfigLocation方法來設(shè)置quartz定時(shí)任務(wù)框架的基本配置,配置文件所在位置:resources/quartz.properties => classpath:/quartz.properties下。

注意:quartz.properties配置文件一定要放在classpath下,放在別的位置有部分功能不會(huì)生效。

下面我們來看下quartz.properties文件內(nèi)的配置,如下所示:

#調(diào)度器實(shí)例名稱
org.quartz.scheduler.instanceName = quartzScheduler

#調(diào)度器實(shí)例編號(hào)自動(dòng)生成
org.quartz.scheduler.instanceId = AUTO

#持久化方式配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

#持久化方式配置數(shù)據(jù)驅(qū)動(dòng),MySQL數(shù)據(jù)庫
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#quartz相關(guān)數(shù)據(jù)表前綴名
org.quartz.jobStore.tablePrefix = QRTZ_

#開啟分布式部署
org.quartz.jobStore.isClustered = true
#配置是否使用
org.quartz.jobStore.useProperties = false

#分布式節(jié)點(diǎn)有效性檢查時(shí)間間隔,單位:毫秒
org.quartz.jobStore.clusterCheckinInterval = 20000

#線程池實(shí)現(xiàn)類
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

#執(zhí)行最大并發(fā)線程數(shù)量
org.quartz.threadPool.threadCount = 10

#線程優(yōu)先級(jí)
org.quartz.threadPool.threadPriority = 5

#配置為守護(hù)線程,設(shè)置后任務(wù)將不會(huì)執(zhí)行
#org.quartz.threadPool.makeThreadsDaemons=true

#配置是否啟動(dòng)自動(dòng)加載數(shù)據(jù)庫內(nèi)的定時(shí)任務(wù),默認(rèn)true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

由于我們下一章需要做分布式多節(jié)點(diǎn)自動(dòng)交付高可用,本章的配置文件加入了分布式相關(guān)的配置。
在上面配置中org.quartz.jobStore.classorg.quartz.jobStore.driverDelegateClass是定時(shí)任務(wù)持久化的關(guān)鍵配置,配置了數(shù)據(jù)庫持久化定時(shí)任務(wù)以及采用MySQL數(shù)據(jù)庫進(jìn)行連接,當(dāng)然這里我們也可以配置其他的數(shù)據(jù)庫,如下所示:
PostgreSQLorg.quartz.impl.jdbcjobstore.PostgreSQLDelegate
Sybase : org.quartz.impl.jdbcjobstore.SybaseDelegate
MSSQL : org.quartz.impl.jdbcjobstore.MSSQLDelegate
HSQLDB : org.quartz.impl.jdbcjobstore.HSQLDBDelegate
Oracle : org.quartz.impl.jdbcjobstore.oracle.OracleDelegate

org.quartz.jobStore.tablePrefix屬性配置了定時(shí)任務(wù)數(shù)據(jù)表的前綴,在quartz官方提供的創(chuàng)建表SQL腳本默認(rèn)就是qrtz_,在對(duì)應(yīng)的XxxDelegate驅(qū)動(dòng)類內(nèi)也是使用的默認(rèn)值,所以這里我們?nèi)绻薷谋砻熬Y,配置可以去掉。

org.quartz.jobStore.isClustered屬性配置了開啟定時(shí)任務(wù)分布式功能,再開啟分布式時(shí)對(duì)應(yīng)屬性org.quartz.scheduler.instanceId 改成Auto配置即可,實(shí)例唯一標(biāo)識(shí)會(huì)自動(dòng)生成,這個(gè)標(biāo)識(shí)具體生成的內(nèi)容,我們一會(huì)在運(yùn)行的控制臺(tái)就可以看到了,定時(shí)任務(wù)分布式準(zhǔn)備好后會(huì)輸出相關(guān)的分布式節(jié)點(diǎn)配置信息。

創(chuàng)建表SQL會(huì)在本章源碼resources目錄下,源碼地址https://gitee.com/hengboy/spring-boot-chapter。

準(zhǔn)備測(cè)試

我們先來創(chuàng)建一個(gè)簡(jiǎn)單的商品數(shù)據(jù)表,建表SQL如下所示:

DROP TABLE IF EXISTS `basic_good_info`;
CREATE TABLE `basic_good_info` (
  `BGI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品編號(hào)',
  `BGI_NAME` varchar(20) DEFAULT NULL COMMENT '商品名稱',
  `BGI_PRICE` decimal(8,2) DEFAULT NULL COMMENT '單價(jià)',
  `BGI_UNIT` varchar(10) DEFAULT NULL COMMENT '單位',
  PRIMARY KEY (`BGI_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='商品基本信息';

GoodEntity

我們先來針對(duì)表basic_good_info創(chuàng)建一個(gè)實(shí)體,并且添加JPA相關(guān)的配置,如下所示:

package com.hengyu.chapter39.good.entity;

import lombok.Data;

import javax.persistence.*;
import java.math.BigDecimal;

/**
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:14:59
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Entity
@Table(name = "basic_good_info")
@Data
public class GoodInfoEntity
{
    /**
     * 商品編號(hào)
     */
    @Id
    @GeneratedValue
    @Column(name = "bgi_id")
    private Long id;
    /**
     * 商品名稱
     */
    @Column(name = "bgi_name")
    private String name;
    /**
     * 商品單位
     */
    @Column(name = "bgi_unit")
    private String unit;
    /**
     * 商品單價(jià)
     */
    @Column(name = "bgi_price")
    private BigDecimal price;
}

下面我們根據(jù)商品實(shí)體來創(chuàng)建JPA接口,如下所示:

/**
 * ========================
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:14:55
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author 恒宇少年
 */
public interface GoodInfoRepository
    extends JpaRepository<GoodInfoEntity,Long>
{
}

接下來我們?cè)賮硖砑右粋€(gè)商品添加的控制器方法,如下所示:

/**
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:02
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@RestController
@RequestMapping(value = "/good")
public class GoodController
{
    /**
     * 商品業(yè)務(wù)邏輯實(shí)現(xiàn)
     */
    @Autowired
    private GoodInfoService goodInfoService;
    /**
     * 添加商品
     * @return
     */
    @RequestMapping(value = "/save")
    public Long save(GoodInfoEntity good) throws Exception
    {
        return goodInfoService.saveGood(good);
    }
}

在請(qǐng)求商品添加方法時(shí),我們調(diào)用了GoodInfoService內(nèi)的saveGood方法,傳遞一個(gè)商品的實(shí)例作為參數(shù)。我們接下來看看該類內(nèi)相關(guān)代碼,如下所示:

/**
 * 商品業(yè)務(wù)邏輯
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:04
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class GoodInfoService
{
    /**
     * 注入任務(wù)調(diào)度器
     */
    @Autowired
    private Scheduler scheduler;
    /**
     * 商品數(shù)據(jù)接口
     */
    @Autowired
    private GoodInfoRepository goodInfoRepository;

    /**
     * 保存商品基本信息
     * @param good 商品實(shí)例
     * @return
     */
    public Long saveGood(GoodInfoEntity good) throws Exception
    {
        goodInfoRepository.save(good);
        return good.getId();
    }

我們只是作為保存商品的操作,下面我們來模擬一個(gè)需求,在商品添加完成后1分鐘我們通知后續(xù)的邏輯進(jìn)行下一步處理,同時(shí)開始商品庫存定時(shí)檢查的任務(wù)。

定義商品添加定時(shí)任務(wù)

我們先來創(chuàng)建一個(gè)任務(wù)實(shí)例,并且繼承org.springframework.scheduling.quartz.QuartzJobBean抽象類,重寫父抽象類內(nèi)的executeInternal方法來實(shí)現(xiàn)任務(wù)的主體邏輯。如下所示:

/**
 * 商品添加定時(shí)任務(wù)實(shí)現(xiàn)類
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/11/5
 * Time:14:47
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author 恒宇少年
 */
public class GoodAddTimer
    extends QuartzJobBean
{
    /**
     * logback
     */
    static Logger logger = LoggerFactory.getLogger(GoodAddTimer.class);
    /**
     * 定時(shí)任務(wù)邏輯實(shí)現(xiàn)方法
     * 每當(dāng)觸發(fā)器觸發(fā)時(shí)會(huì)執(zhí)行該方法邏輯
     * @param jobExecutionContext 任務(wù)執(zhí)行上下文
     * @throws JobExecutionException
     */
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        logger.info("商品添加完成后執(zhí)行任務(wù),任務(wù)時(shí)間:{}",new Date());
    }

在任務(wù)主體邏輯內(nèi),我們只是做了一個(gè)簡(jiǎn)單的輸出任務(wù)執(zhí)行的時(shí)間,下面我們?cè)賮韯?chuàng)建庫存定時(shí)檢查任務(wù)。

定義商品庫存檢查任務(wù)

同樣需要繼承org.springframework.scheduling.quartz.QuartzJobBean抽象類實(shí)現(xiàn)抽象類內(nèi)的executeInternal方法,如下所示:

/**
 * 商品庫存檢查定時(shí)任務(wù)
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:47
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
public class GoodStockCheckTimer
    extends QuartzJobBean
{
    /**
     * logback
     */
    static Logger logger = LoggerFactory.getLogger(GoodStockCheckTimer.class);

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        logger.info("執(zhí)行庫存檢查定時(shí)任務(wù),執(zhí)行時(shí)間:{}",new Date());
    }
}

都是簡(jiǎn)單的做了下日志的輸出,下面我們需要重構(gòu)GoodInfoService內(nèi)的saveGood方法,對(duì)應(yīng)的添加上面兩個(gè)任務(wù)的創(chuàng)建。

設(shè)置商品添加任務(wù)到調(diào)度器

GoodInfoService類內(nèi)添加buildCreateGoodTimer方法用于實(shí)例化商品添加任務(wù),如下所示:

/**
     * 構(gòu)建創(chuàng)建商品定時(shí)任務(wù)
     */
    public void buildCreateGoodTimer() throws Exception
    {
        //設(shè)置開始時(shí)間為1分鐘后
        long startAtTime = System.currentTimeMillis() + 1000 * 60;
        //任務(wù)名稱
        String name = UUID.randomUUID().toString();
        //任務(wù)所屬分組
        String group = GoodAddTimer.class.getName();
        //創(chuàng)建任務(wù)
        JobDetail jobDetail = JobBuilder.newJob(GoodAddTimer.class).withIdentity(name,group).build();
        //創(chuàng)建任務(wù)觸發(fā)器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name,group).startAt(new Date(startAtTime)).build();
        //將觸發(fā)器與任務(wù)綁定到調(diào)度器內(nèi)
        scheduler.scheduleJob(jobDetail, trigger);
    }

在上面方法中我們定義的GoodAddTimer實(shí)例只運(yùn)行一次,在商品添加完成后延遲1分鐘進(jìn)行調(diào)用任務(wù)主體邏輯。

其中任務(wù)的名稱以及任務(wù)的分組是為了區(qū)分任務(wù)做的限制,在同一個(gè)分組下如果加入同樣名稱的任務(wù),則會(huì)提示任務(wù)已經(jīng)存在,添加失敗的提示。

我們通過JobDetail來構(gòu)建一個(gè)任務(wù)實(shí)例,設(shè)置GoodAddTimer類作為任務(wù)運(yùn)行目標(biāo)對(duì)象,當(dāng)任務(wù)被觸發(fā)時(shí)就會(huì)執(zhí)行GoodAddTimer內(nèi)的executeInternal方法。

一個(gè)任務(wù)需要設(shè)置對(duì)應(yīng)的觸發(fā)器,觸發(fā)器也分為很多種,該任務(wù)中我們并沒有采用cron表達(dá)式來設(shè)置觸發(fā)器,而是調(diào)用startAt方法設(shè)置任務(wù)開始執(zhí)行時(shí)間。

最后將任務(wù)以及任務(wù)的觸發(fā)器共同交付給任務(wù)調(diào)度器,這樣就完成了一個(gè)任務(wù)的設(shè)置。

設(shè)置商品庫存檢查到任務(wù)調(diào)度器

GoodInfoService類內(nèi)添加buildGoodStockTimer方法用于實(shí)例化商品添加任務(wù),如下所示:

/**
     * 構(gòu)建商品庫存定時(shí)任務(wù)
     * @throws Exception
     */
    public void buildGoodStockTimer() throws Exception
    {
        //任務(wù)名稱
        String name = UUID.randomUUID().toString();
        //任務(wù)所屬分組
        String group = GoodStockCheckTimer.class.getName();

        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/30 * * * * ?");
        //創(chuàng)建任務(wù)
        JobDetail jobDetail = JobBuilder.newJob(GoodStockCheckTimer.class).withIdentity(name,group).build();
        //創(chuàng)建任務(wù)觸發(fā)器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name,group).withSchedule(scheduleBuilder).build();
        //將觸發(fā)器與任務(wù)綁定到調(diào)度器內(nèi)
        scheduler.scheduleJob(jobDetail, trigger);
    }

該任務(wù)的觸發(fā)器我們采用了cron表達(dá)式來設(shè)置,每隔30秒執(zhí)行一次任務(wù)主體邏輯。

任務(wù)觸發(fā)器在創(chuàng)建時(shí)cron表達(dá)式可以搭配startAt方法來同時(shí)使用。

下面我們修改GoodInfoService內(nèi)的saveGood方法,分別調(diào)用設(shè)置任務(wù)的兩個(gè)方法,如下所示:

/**
     * 保存商品基本信息
     * @param good 商品實(shí)例
     * @return
     */
    public Long saveGood(GoodInfoEntity good) throws Exception
    {
        goodInfoRepository.save(good);
        //構(gòu)建創(chuàng)建商品定時(shí)任務(wù)
        buildCreateGoodTimer();
        //構(gòu)建商品庫存定時(shí)任務(wù)
        buildGoodStockTimer();
        return good.getId();
    }

下面我們就來測(cè)試下任務(wù)是否可以順序的被持久化到數(shù)據(jù)庫,并且是否可以在重啟服務(wù)后執(zhí)行重啟前添加的任務(wù)。

測(cè)試

下面我們來啟動(dòng)項(xiàng)目,啟動(dòng)成功后,我們來查看控制臺(tái)輸出的分布式節(jié)點(diǎn)的信息,如下所示:

2017-11-05 18:09:40.052  INFO 7708 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時(shí)任務(wù)分布式節(jié)點(diǎn) - 1 已啟動(dòng)】】】】】】
2017-11-05 18:09:42.005  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-05 18:09:42.027  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-05 18:09:42.027  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1509876084785"'s failed in-progress jobs.
2017-11-05 18:09:42.031  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: ......Freed 1 acquired trigger(s).
2017-11-05 18:09:42.033  INFO 7708 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1509876579404 started.

定時(shí)任務(wù)是在項(xiàng)目啟動(dòng)后2秒進(jìn)行執(zhí)行初始化,并且通過ClusterManager來完成了instance的創(chuàng)建,創(chuàng)建的節(jié)點(diǎn)唯一標(biāo)識(shí)為yuqiyu1509876084785

編寫商品控制器請(qǐng)求方法測(cè)試用例,如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter39ApplicationTests {
    /**
     * 模擬mvc測(cè)試對(duì)象
     */
    private MockMvc mockMvc;

    /**
     * web項(xiàng)目上下文
     */
    @Autowired
    private WebApplicationContext webApplicationContext;

    /**
     * 所有測(cè)試方法執(zhí)行之前執(zhí)行該方法
     */
    @Before
    public void before() {
        //獲取mockmvc對(duì)象實(shí)例
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    /**
     * 測(cè)試添加商品
     * @throws Exception
     */
    @Test
    public void addGood() throws Exception
    {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/good/save")
                .param("name","西瓜")
                .param("unit","斤")
                .param("price","12.88")
        )
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().is(200))
                .andReturn();
        result.getResponse().setCharacterEncoding("UTF-8");
        System.out.println(result.getResponse().getContentAsString());
    }

測(cè)試用例相關(guān)文章請(qǐng)?jiān)L問第三十五章:SpringBoot與單元測(cè)試的小秘密,我們來執(zhí)行addGood測(cè)試方法,查看控制臺(tái)輸出,如下所示:

....省略部分輸出
Hibernate: insert into basic_good_info (bgi_name, bgi_price, bgi_unit) values (?, ?, ?)
2017-11-05 18:06:35.699 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [西瓜]
2017-11-05 18:06:35.701 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [NUMERIC] - [12.88]
2017-11-05 18:06:35.701 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [斤]
....省略部分輸出
8
....省略部分輸出

可以看到我們的商品已被成功的寫入到數(shù)據(jù)庫并且輸出的主鍵值,我們的任務(wù)是否也成功的被寫入到數(shù)據(jù)庫了呢?我們來查看qrtz_job_details表內(nèi)任務(wù)列表,如下所示:

schedulerFactoryBean    7567c9d7-76f5-47f3-bc5d-b934f4c1063b    com.hengyu.chapter39.timers.GoodStockCheckTimer     com.hengyu.chapter39.timers.GoodStockCheckTimer 0   0   0   0   0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800
schedulerFactoryBean    e5e08ab0-9be3-43fb-93b8-b9490432a5d7    com.hengyu.chapter39.timers.GoodAddTimer        com.hengyu.chapter39.timers.GoodAddTimer    0   0   0   0   0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800

任務(wù)已經(jīng)被成功的持久化到數(shù)據(jù)庫內(nèi),等待1分鐘后查看控制臺(tái)輸出內(nèi)容如下所示:

2017-11-05 18:12:30.017  INFO 7708 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時(shí)任務(wù),執(zhí)行時(shí)間:Sun Nov 05 18:12:30 CST 2017
2017-11-05 18:13:00.009  INFO 7708 --- [ryBean_Worker-2] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時(shí)任務(wù),執(zhí)行時(shí)間:Sun Nov 05 18:13:00 CST 2017
2017-11-05 18:13:02.090  INFO 7708 --- [ryBean_Worker-3] c.hengyu.chapter39.timers.GoodAddTimer   : 商品添加完成后執(zhí)行任務(wù),任務(wù)時(shí)間:Sun Nov 05 18:13:02 CST 2017

根據(jù)輸出的內(nèi)容來判定完全吻合我們的配置參數(shù),庫存檢查為30秒執(zhí)行一次,而添加成功后的提醒則是1分鐘后執(zhí)行一次。執(zhí)行完成后就會(huì)被直接銷毀,我們?cè)賮聿榭磾?shù)據(jù)庫表qrtz_job_details,這時(shí)就可以看到還剩下1個(gè)任務(wù)。

重啟服務(wù)任務(wù)是否自動(dòng)執(zhí)行?

下面我們把項(xiàng)目重啟下,然后觀察控制臺(tái)的輸出內(nèi)容,如下所示:

2017-11-05 18:15:54.018  INFO 7536 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時(shí)任務(wù)分布式節(jié)點(diǎn) - 1 已啟動(dòng)】】】】】】
2017-11-05 18:15:55.975  INFO 7536 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-05 18:15:56.000  INFO 7536 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1509876953202 started.
2017-11-05 18:16:15.999  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-05 18:16:16.000  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1509876579404"'s failed in-progress jobs.
2017-11-05 18:16:16.005  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: ......Freed 1 acquired trigger(s).
2017-11-05 18:16:16.041  INFO 7536 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時(shí)任務(wù),執(zhí)行時(shí)間:Sun Nov 05 18:16:16 CST 2017

可以看到成功的自動(dòng)執(zhí)行了我們?cè)谥貑⒅芭渲玫娜蝿?wù)。

總結(jié)

本章主要講解了SpringBoot整合quartz定時(shí)任務(wù)框架,完成了分布式單節(jié)點(diǎn)任務(wù)持久化,下一章我們會(huì)講解任務(wù)參數(shù)傳遞以及分布式多節(jié)點(diǎn)任務(wù)自動(dòng)負(fù)載。

本章源碼已經(jīng)上傳到碼云:
SpringBoot配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter

作者個(gè)人 博客
使用開源框架 ApiBoot 助你成為Api接口服務(wù)架構(gòu)師

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容