定時任務發展史(一)

純潔的微笑發表於2017-07-05

定時任務是網際網路行業裡最常用的服務之一,本文給大家介紹定時任務在我司的發展歷程。

linux系統中一般使用crontab命令來實現,在Java世界裡,使用最廣泛的就是quartz了。我司使用quartz就已經升級了三代,每一代在上一代系統之上有所優化,寫這篇文章一方面介紹一下quartz的使用,另一方面可以根據此專案的變遷反應出我司平臺架構升級的一個縮影。

定時任務的使用場景很多,以我們平臺來講:計息,派息、對賬等等。

quartz 介紹

Quartz是個開源的作業排程框架,為在Java應用程式中進行作業排程提供了簡單卻強大的機制。Quartz允許開發人員根據時間間隔(或天)來排程作業。它實現了作業和觸發器的多對多關係,還能把多個作業與不同的觸發器關聯。Quartz可以整合幾乎任何的java應用程式—從小的微控制器系統到大型的電子商務系統。Quartz可以執行上千上萬的任務排程。

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 * * ?

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

具體時間設定可參考 :

"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');

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

  • 1、定時排程和業務程式碼耦合在一起
  • 2、每次調整定時任務的時間需要重啟服務