一个简单的定时任务调度分发器设计
前言:
设计一个简单的定时任务调度分发器,利用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,这只是一个简单的示例,并不是所有定时任务都需要分发执行;