一個簡單的定時任務調度分發器設計


前言:

  設計一個簡單的定時任務調度分發器,利用spring+quartz,讓系統每5秒鍾去執行“主調度器”job;主調度器job根據數據庫配置去延時執行其他定時任務。

 

1,利用spring+quartz,讓系統每5秒鍾去執行“主調度器”job

  參考 https://www.cnblogs.com/seeall/p/12085159.html ;

 

2,數據庫設計

  2.1,創建一張“任務信息表”:task_info

序號

字段名

字段類型

描述

1

id

int(11) NOT NULL

主鍵ID

2

name

varchar(50) NOT NULL

任務名稱

3

desc

varchar(1000) NULL

任務描述

4

create_time

timestamp NULL

創建時間

5

update_time

timestamp NULL

更新時間

6

status

tinyint(1) NOT NULL

記錄有效性:0-無效,1-有效


    

 

 

 

 

 

 

 

 

    新增3個定時任務: 

id

name

desc

create_time

update_time

status

8

洗衣服任務

每天下午五點四十分和五點四十五分洗衣服,WashClothesServiceImpl

2019-12-24 11:48:16

 

1

9

燒水任務

每天下午五點三十分和五點三十五分燒水,BoilWaterServiceImpl

2019-12-24 11:49:46

 

1

10

做飯任務

每天下午五點三十五分做飯,CookServiceImpl

2019-12-24 11:51:14

 

1

   

  

 

 

 

 

 

  2.2,創建一張“任務配置表”task_config

序號

字段名

字段類型

描述

1

id

int(11) NOT NULL

主鍵ID

2

table

varchar(50) NOT NULL

關聯表名稱

3

table_id

int(11) NOT NULL

關聯表主鍵ID

4

key

varchar(50) NOT NULL

配置項key

5

value

varchar(150) NOT NULL

配置項value

6

create_time

timestamp NULL

創建時間

7

update_time

timestamp NULL

更新時間

8

status

tinyint(1) NOT NULL

記錄有效性:0-無效,1-有效

 

    為2.1中的三個定時任務:洗衣服任務、燒水任務、做飯任務,配置相關選項:觸發表達式,處理任務的類,以及job執行所在服務器ip。

id

table

table_id

key

value

create_time

update_time

status

42

task_info

9

cronExpression

0 30,35 17 * * ?

2019-12-24 15:01:38

 

1

43

task_info

9

service

BoilWaterServiceImpl

2019-12-24 15:03:28

 

1

44

task_info

9

serverIp

127.0.0.1

2019-12-24 15:03:30

 

1

45

task_info

8

cronExpression

0 40,45 17 * * ?

2019-12-24 15:04:35

 

1

46

task_info

8

service

WashClothesServiceImpl

2019-12-24 15:04:35

 

1

47

task_info

8

serverIp

127.0.0.1

2019-12-24 15:04:36

 

1

48

task_info

10

cronExpression

0 35 17 * * ?

2019-12-24 15:04:37

 

1

49

task_info

10

service

CookServiceImpl

2019-12-24 15:04:37

 

1

50

task_info

10

serverIp

127.0.0.1

2019-12-24 15:04:40

 

1

 

 

 

 

 

 

 

 

 

 

 

                                                                                                                           

 

3,三個定時任務實現

  3.1,燒飯任務實現

  @Service(value = "CookServiceImpl")

 public class CookServiceImpl extends AbstractTask {
private static Logger LOGGER = LoggerFactory.getLogger(CookServiceImpl.class);

@Override
   public void execute() throws Exception {
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 開始做飯...");
Thread.sleep(60000);
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 飯做好了!");
}
}

 3.2,燒水任務實現
 @Service(value = "BoilWaterServiceImpl")
 public class BoilWaterServiceImpl extends AbstractTask {
private static Logger LOGGER = LoggerFactory.getLogger(BoilWaterServiceImpl.class);

@Override
   public void execute() throws Exception {
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 開始燒水了...");
Thread.sleep(60000);
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 水燒好了!");
}
 }

 3.3,洗衣服任務實現
 @Service(value = "WashClothesServiceImpl")
   public class WashClothesServiceImpl extends AbstractTask {
private static Logger LOGGER = LoggerFactory.getLogger(WashClothesServiceImpl.class);

@Override
   public void execute() throws Exception {
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 開始洗衣服...");
Thread.sleep(60000);
LOGGER.info("現在時間是" + DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss") + ", 衣服洗好了!");
}
}

        

4,“主調度器”實現

  4.1,查詢所有在本機執行的定時任務列表集合

  SELECT task_info.id AS taskId,task_info.`name` AS taskName,temp_scheduler.`value` AS cronTriggerExpression,temp_service.value AS service

        FROM task_info task_info

        LEFT JOIN(SELECT * FROM task_config WHERE `key` = 'cronExpression' AND `table` = 'task_info') temp_scheduler ON task_info.id = temp_scheduler.table_id

        LEFT JOIN(SELECT * FROM task_config WHERE `key` = 'serverIp' AND `table` = 'task_info') temp_server ON task_info.id = temp_server.table_id

        LEFT JOIN(SELECT * FROM task_config WHERE `key` = 'service' AND `table` = 'task_info') temp_service ON task_info.id = temp_service.table_id

        WHERE task_info.status = 1 AND temp_server.value = '127.0.0.1'

 

  查詢結果:

taskId

taskName

cronTriggerExpression

service

9

燒水任務

0 30,35 17 * * ?

BoilWaterServiceImpl

8

洗衣服任務

0 40,45 17 * * ?

WashClothesServiceImpl

10

做飯任務

0 35 17 * * ?

CookServiceImpl

  

  4.2,循環遍歷這些需要在本機執行的任務,還是參考代碼吧 

/**
* 未執行(或等待延時執行)的任務列表
*/
private final Map<Integer, Scheduler> tasks = new ConcurrentHashMap<Integer, Scheduler>();

 

 // 執行“主調度器”job

public void dispatch() {
try {
// 查詢所有在本機ip執行的定時任務列表
     List<TaskExecuteDetail> taskListRunInThisIP = taskExecuteMapper.listTaskDetailByIP(IPUtils.getLocalIP());
if (CollectionUtils.isEmpty(taskListRunInThisIP)) {
return;
}

for (final TaskExecuteDetail taskExecuteDetail : taskListRunInThisIP) {
final Scheduler scheduler;

if (!tasks.containsKey(taskExecuteDetail.getTaskId())) {
/**
* 如果“待執行任務列表”中不存在該任務(說明任務已經成功執行,因為任務一旦成功執行后,會從“待執行任務列表”中刪除);
* 重新將該任務(數據庫查詢獲得)加入“待執行任務列表”中,並等待該任務在下一次執行時間到達時自動執行
*/

          scheduler = new Scheduler();
scheduler.setTaskId(taskExecuteDetail.getTaskId());
scheduler.setTaskName(taskExecuteDetail.getTaskName());
scheduler.setStringExpression(taskExecuteDetail.getCronTriggerExpression());

// 設置觸發表達式對象:org.quartz.CronExpression
          try {
scheduler.setCronExpression(new CronExpression(taskExecuteDetail.getCronTriggerExpression()));
} catch (ParseException e) {
LOGGER.error("convert String expression to org.quartz.CronExpression fail!", e);
continue;
}

// 這里簡單的設置為單線程執行
          scheduler.setExecutor(Executors.newScheduledThreadPool(1));

// 將該任務加入到“待執行任務列表”中
          tasks.put(taskExecuteDetail.getTaskId(), scheduler);
} else {
/**
* 如果“待執行任務列表”中已經存在該任務(說明任務還未執行,因為任務一旦執行后,會從緩存列表中刪除),
* 1,任務的觸發表達式改變,需要更新“待執行任務列表”中的對應的任務對象,並重新(調整延時時間)執行該任務;
* 2,任務的觸發表達式沒有改變,則無需執行該任務,等待該任務在下一次執行時間到達時自動執行;
* 按道理,serverIp和service也有可能改變,這邊簡單處理,就不考慮了
*/

          scheduler = tasks.get(taskExecuteDetail.getTaskId());
          
          // 如果該任務仍然在“待執行任務”列表中,則continue跳過,不做任何操作;因為到點了,該任務自然會去執行
          if (StringUtils.isBlank(taskExecuteDetail.getCronTriggerExpression())
        || taskExecuteDetail.getCronTriggerExpression().equals(scheduler.getStringExpression())) {
                    continue;
}

scheduler.setStringExpression(taskExecuteDetail.getCronTriggerExpression());

// 設置觸發表達式對象:org.quartz.CronExpression
          try {
scheduler.setCronExpression(new CronExpression(taskExecuteDetail.getCronTriggerExpression()));
} catch (ParseException e) {
LOGGER.error("convert String expression to org.quartz.CronExpression fail!", e);
continue;
}

scheduler.getExecutor().shutdownNow();
scheduler.setExecutor(Executors.newScheduledThreadPool(1));
}

// 獲取該任務下一次執行的時間(有效時間)
       final Date current = new Date();
final Date next = scheduler.getCronExpression().getNextValidTimeAfter(current);
scheduler.setValid(next);

// 延時(next.getTime() - current.getTime())毫秒后執行這個任務
       scheduler.getExecutor().schedule(new Runnable() {
public void run() {
LOGGER.info("任務-" + scheduler.getTaskName() + ",將在" + (next.getTime() - current.getTime())/1000 + "秒后執行");
AbstractTask task = (AbstractTask) applicationContext.getBean(taskExecuteDetail.getService());
try {
task.execute();
} catch (Exception e) {
LOGGER.error("execute task fail! task = " + task, e);
} finally {
// 一旦該任務在設置的時間執行了,將其從“待執行任務列表”中移除
               scheduler.getExecutor().shutdownNow();
tasks.remove(scheduler.getTaskId());
}
}
}, next.getTime() - current.getTime(), TimeUnit.MILLISECONDS);
}
} catch (YourProgramException ype) {
// do something
} catch (Exception e) {
// do something
}
}

                                  AbstractTask是一個抽象類,目的是為了多態

 

 

 

5,執行結果

2019-12-24 17:30:00.005 [pool-2-thread-1] INFO  c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在48秒后執行

2019-12-24 17:30:00.026 [pool-2-thread-1] INFO  c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 17:30:00, 開始燒水了...

2019-12-24 17:31:00.028 [pool-2-thread-1] INFO  c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 17:31:00, 水燒好了!

2019-12-24 17:35:00.004 [pool-5-thread-1] INFO  c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在239秒后執行

2019-12-24 17:35:00.005 [pool-5-thread-1] INFO  c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 17:35:00, 開始燒水了...

2019-12-24 17:35:00.006 [pool-4-thread-1] INFO  c.s.s.service.timertask.schedule.TaskDispatcher - 任務-做飯任務,將在348秒后執行

2019-12-24 17:35:00.006 [pool-4-thread-1] INFO  c.s.s.s.monitor.timer.service.impl.CookServiceImpl - 現在時間是2019-12-24 17:35:00, 開始做飯...

2019-12-24 17:36:00.006 [pool-5-thread-1] INFO  c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 17:36:00, 水燒好了!

2019-12-24 17:36:00.009 [pool-4-thread-1] INFO  c.s.s.s.monitor.timer.service.impl.CookServiceImpl - 現在時間是2019-12-24 17:36:00, 飯做好了!

2019-12-24 17:40:00.013 [pool-3-thread-1] INFO  c.s.s.service.timertask.schedule.TaskDispatcher - 任務-洗衣服任務,將在648秒后執行

2019-12-24 17:40:00.013 [pool-3-thread-1] INFO  c.s.s.s.m.t.service.impl.WashClothesServiceImpl - 現在時間是2019-12-24 17:40:00, 開始洗衣服...

2019-12-24 17:41:00.014 [pool-3-thread-1] INFO  c.s.s.s.m.t.service.impl.WashClothesServiceImpl - 現在時間是2019-12-24 17:41:00, 衣服洗好了!

2019-12-24 17:45:00.025 [pool-8-thread-1] INFO  c.s.s.service.timertask.schedule.TaskDispatcher - 任務-洗衣服任務,將在239秒后執行

2019-12-24 17:45:00.026 [pool-8-thread-1] INFO  c.s.s.s.m.t.service.impl.WashClothesServiceImpl - 現在時間是2019-12-24 17:45:00, 開始洗衣服...

2019-12-24 17:46:00.026 [pool-8-thread-1] INFO  c.s.s.s.m.t.service.impl.WashClothesServiceImpl - 現在時間是2019-12-24 17:46:00, 衣服洗好了!

 

tips:

  1,如果上面代碼中標紅的continue沒有達到跳過的作用,並且線程設置的不止一個,那么定時任務將會被執行多次: 

2019-12-24 19:58:00.003 [pool-2-thread-3] INFO c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在4秒后執行
2019-12-24 19:58:00.003 [pool-2-thread-3] INFO c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 19:58:00, 開始燒水了...
2019-12-24 19:58:00.003 [pool-2-thread-4] INFO c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在44秒后執行
2019-12-24 19:58:00.003 [pool-2-thread-4] INFO c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 19:58:00, 開始燒水了...
2019-12-24 19:58:00.003 [pool-2-thread-5] INFO c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在49秒后執行
2019-12-24 19:58:00.003 [pool-2-thread-5] INFO c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 19:58:00, 開始燒水了...
2019-12-24 19:58:00.003 [pool-2-thread-6] INFO c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在9秒后執行
2019-12-24 19:58:00.003 [pool-2-thread-6] INFO c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 19:58:00, 開始燒水了...
2019-12-24 19:58:00.003 [pool-2-thread-7] INFO c.s.s.service.timertask.schedule.TaskDispatcher - 任務-燒水任務,將在34秒后執行
2019-12-24 19:58:00.003 [pool-2-thread-7] INFO c.s.s.s.m.timer.service.impl.BoilWaterServiceImpl - 現在時間是2019-12-24 19:58:00, 開始燒水了...

  所以,這種分發定時任務的方式還是存在一定風險的,避免這種風險,需要業務代碼邏輯謹慎;設置成單線程也是一種比較保險的方法!!

  2,其他功能都可以在此基礎上擴展,如代理服務器組,多線程執行等等;

  3,這只是一個簡單的示例,並不是所有定時任務都需要分發執行;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM