前言:
設計一個簡單的定時任務調度分發器,利用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,這只是一個簡單的示例,並不是所有定時任務都需要分發執行;