定時任務發展史(一)

定時任務是互聯網行業裏最經常使用的服務之一,本文給你們介紹定時任務在我司的發展歷程。java

linux系統中通常使用crontab命令來實現,在Java世界裏,使用最普遍的就是quartz了。我司使用quartz就已經升級了三代,每一代在上一代系統之上有所優化,寫這篇文章一方面介紹一下quartz的使用,另外一方面能夠根據此項目的變遷反應出我司平臺架構升級的一個縮影。linux

定時任務的使用場景不少,以咱們平臺來說:計息,派息、對帳等等。spring

quartz 介紹

Quartz是個開源的做業調度框架,爲在Java應用程序中進行做業調度提供了簡單卻強大的機制。Quartz容許開發人員根據時間間隔(或天)來調度做業。它實現了做業和觸發器的多對多關係,還能把多個做業與不一樣的觸發器關聯。Quartz能夠集成幾乎任何的java應用程序—從小的單片機系統到大型的電子商務系統。Quartz能夠執行上千上萬的任務調度。sql

Quartz核心的概念:scheduler任務調度、Job任務、JobDetail任務細節、Trigger觸發器架構

  • Scheduler:調度器,調度器接受一組JobDetail+Trigger便可安排一個任務,其中一個JobDetail能夠關聯多個Trigger
  • Job:Job是任務執行的流程,是一個類
  • JobDetail:JobDetail是Job是實例,是一個對象,包含了該實例的執行計劃和所須要的數據
  • Trigger:Trigger是定時器,決定任務什麼時候執行

使用Quartz調度系統的思路就是,首先寫一個具體的任務(job),配置任務的觸發時間(Trigger),Scheduler很根據JobDetail+Trigger安排去執行此任務。框架

Quartz 定時器的時間設置優化

時間的配置以下: 0 30 16 ? ui

時間大小由小到大排列,從秒開始,順序爲 秒,分,時,天,月,年 *爲任意 ?爲無限制。由此上面所配置的內容就是,在天天的16點30分啓動buildSendHtml() 方法this

具體時間設定可參考 :編碼

"0/10 ?" 每10秒觸發
"0 0 12 ?" 天天中午12點觸發
"0 14 ?" 在天天下午2點到下午2:59期間的每1分鐘觸發
"0 10,44 14 ? 3 WED" 每一年三月的星期三的下午2:10和2:44觸發
"0 15 10 ?
MON-FRI" 週一至週五的上午10:15觸發
"0 0 06,18 ?" 在天天上午6點和下午6點觸發

第一代定時任務系統

第一代定時任務系統使用的很簡單,所有按照當時spring推薦的配置方式來進行,開發於2014年初。

首先在配置線程池

<bean id="executor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
     <property name="corePoolSize" value="50" />
     <property name="maxPoolSize" value="100" />
     <property name="queueCapacity" value="500" />
</bean>複製代碼

配置定時任務工廠和任務基類

<bean id="timerFactory" class="com.zx.timer.TimerFactory" />

<bean id="baseTask" class="com.zx.timer.core.BaseTask">
    <property name="machineId" value="${machine.id}"/>
    <property name="recordErrorDetail" value="${is.record.errordetail}"/>
</bean>複製代碼
  • machineId:機器編碼
  • recordErrorDetail:是否記錄詳細日誌

經過timerFactory 來獲取具體的任務和觸發器

public class TimerFactory implements BeanFactoryAware {

    private BeanFactory beanFactory;

    public Object getTask(String taskCode) {
        return beanFactory.getBean(taskCode+"Task");
    }

    public Object getTrigger(String taskCode) {
        return beanFactory.getBean(taskCode+"Trigger");
    }

    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    public BeanFactory getBeanFactory() {
        return beanFactory;
    }
}複製代碼

baseTask集成了task,在裏面作了一些基礎的業務,好比定時任務開始執行的時候記錄定時任務的開始執行時間,定時任務結束的時候記錄執行的結果等。

public interface Task {
    public void executeTask();
}複製代碼

配置具體的定時任務。以重發短信郵件的定時任務爲例

<bean id="resendSmsAndEmailTask" class="com.zx.timer.core.tasks.ResendSmsAndEmailTask" parent="baseTask">
</bean>

<bean id="resendSmsAndEmailJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="resendSmsAndEmailTask" />
    <property name="targetMethod" value="executeTask" />
    <property name="concurrent" value="false" />
</bean>

<bean id="resendSmsAndEmailTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="resendSmsAndEmailJob" />
    <property name="cronExpression">
        <value>0 0 0 * * ?</value>
    </property>
</bean>複製代碼
  • resendSmsAndEmailTask:具體的定時任務類
  • resendSmsAndEmailJob:包裝成具體的Job
  • resendSmsAndEmailTrigger:設置具體執行的時間,包裝成Trigger

具體的task類,刪掉了部分業務代碼:

public class ResendSmsAndEmailTask extends BaseTask{
    private static final String TASK_CODE = "resendSmsAndEmail";
    AtomicInteger ai = new AtomicInteger(0);

    public void execute(){
        try {
            ai = new AtomicInteger(0);
            // todo
        }catch (Exception e) {
            String exception = ExceptionUtils.getStackTrace(e);
            logger.error("stat error with exception[{}].", exception);
            this.recordTaskErrorDetail(this.taskRecordId, "ResendSmsAndEmailTask-" + e.getMessage(), exception);
        }finally{
            this.modifyTaskRecord(ai.get(), taskRecordId);
        }
    }

    public String getTaskNo() {
        return TASK_CODE;
    }
}複製代碼

最後配置scheduler任務調度

<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="resendSmsAndEmailTrigger" />
        </list>
    </property>
    <property name="taskExecutor" ref="executor" />
</bean>
<bean class="com.zx.timer.core.scheduler.DynamicJobAssembler" init-method="init" scope="singleton"/>複製代碼

DynamicJobAssembler類代碼:

public class DynamicJobAssembler {

    private static Logger logger = LoggerFactory.getLogger(DynamicJobAssembler.class);

    @Resource
    Scheduler scheduler;

    @Resource
    TimerFactory timerFactory;

    @Resource
    TaskDao taskDao;

    public void init() {
        logger.info("start to assemble task from db.");
        List<TaskEntity> tasks = this.taskDao.getAllTask();
        if (tasks == null || tasks.size() <= 0) {
            return;
        }

        Map<String, String> jobNameMap = this.getAllJobNames();
        for (TaskEntity task : tasks) {
            logger.debug(task.toString());
            CronTriggerBean taskTrigger = (CronTriggerBean) timerFactory.getTrigger(task.getTaskNo());
            if (taskTrigger != null) {
                if (!task.getSchedulerRule().equals(taskTrigger.getCronExpression())) {
                    try {
                        taskTrigger.setCronExpression(task.getSchedulerRule());
                    } catch (ParseException e) {
                        logger.error("db task's cronExpression parse error:{}", e.getMessage());
                    }
                    try {
                        logger.info("rescheduleJob jobName:{}",task.getTaskNo());
                        scheduler.rescheduleJob(task.getTaskNo() + "Trigger", Scheduler.DEFAULT_GROUP, taskTrigger);
                    } catch (SchedulerException e) {
                        logger.error("revieved task[{},{}] reschedule error:{}", task.getTaskNo(), task.getSchedulerRule(), e.getMessage());
                    }
                }
                jobNameMap.remove(task.getTaskNo() + "Job");
            }
        }

        if (jobNameMap != null) {
            logger.info("=====================================");
            logger.info("Jobs need to be removed:" + Arrays.toString(jobNameMap.keySet().toArray()));
            logger.info("=====================================");
            for (String jobName : jobNameMap.keySet()) {
                try {
                    scheduler.deleteJob(jobName, jobNameMap.get(jobName));
                } catch (SchedulerException e) {
                    logger.error("Error occured when deleting Job[{}] with Exception:{}", jobName, e.getMessage());
                }
            }
        }
        logger.info("end to assemble task from db.");
    }

    private Map<String, String> getAllJobNames() {
        Map<String, String> jobNameMap = new HashMap<String, String>();
        try {
            String[] groups = scheduler.getJobGroupNames();
            for (String group : groups) {
                String[] jobs = scheduler.getJobNames(group);
                if (jobs != null) {
                    for (String job : jobs) {
                        jobNameMap.put(job, group);
                    }
                }
            }
        } catch (SchedulerException e1) {
            logger.error("Failed in geting all job names with exception:{}", e1.getMessage());
        }
        return jobNameMap;
    }

}複製代碼

定時任務表,執行的時候以表裏面的數據爲準,方便編輯。

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `zx_task_informations`
-- ----------------------------
DROP TABLE IF EXISTS `zx_task_informations`;
CREATE TABLE `zx_task_informations` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL COMMENT '版本號:須要樂觀鎖控制',
  `taskNo` varchar(64) NOT NULL COMMENT '任務編號',
  `taskName` varchar(64) NOT NULL COMMENT '任務名稱',
  `schedulerRule` varchar(64) NOT NULL COMMENT '定時規則表達式',
  `frozenStatus` varchar(16) NOT NULL COMMENT '凍結狀態',
  `executorNo` varchar(128) NOT NULL COMMENT '執行方',
  `timeKey` varchar(32) NOT NULL COMMENT '執行時間格式',
  `frozenTime` bigint(13) DEFAULT NULL COMMENT '凍結時間',
  `unfrozenTime` bigint(13) DEFAULT NULL COMMENT '解凍時間',
  `createTime` bigint(13) NOT NULL COMMENT '建立時間',
  `lastModifyTime` bigint(13) DEFAULT NULL COMMENT '最近修改時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8 COMMENT='定時任務信息表';

-- ----------------------------
-- Records of zx_task_informations
-- ----------------------------
INSERT INTO `zx_task_informations` VALUES ('1', '0', 'resendSmsAndEmail', '重發短信和郵件', '10 */10 * * * ?', 'FROZEN', '0', 'yyyy-MM-dd HH:mm', '0', '0', '0', '1486807296009');複製代碼

這就是咱們第一代定時任務系統,達到了按期執行定時任務的效果,可是一樣有兩個缺點:

  • 一、定時調度和業務代碼耦合在一塊兒
  • 二、每次調整定時任務的時間須要重啓服務

喜歡個人文章,請關注個人公衆號

相關文章
相關標籤/搜索