一个简单的定时任务调度分发器设计

前言:

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

posted @ 2019-12-24 18:30  seeAll  阅读(1470)  评论(0编辑  收藏  举报