那些年我们用过的定时调度

42次阅读

共计 5600 个字符,预计需要花费 14 分钟才能阅读完成。

定时调度

作为后端开发人员,我们总会遇到这样的业务场景:每周同步一批数据;每半个小时检查一遍服务器运行状况;每天早上八点给用户发送一份包含今日待办事项的邮件,等等。

这些场景中都离不开“定时器”,就像一个定好时间规则的闹钟,它会在指定时间触发,执行我们想要定义的调度任务。那么我们今天就来数一下,那些年我们用过的“定时调度”。

1. job (oracle)

从刚工作就一直使用 oracle 数据库,最早接触的定时任务就是 oracle 数据库的 job。job 有定时执行的功能,可以在指定的时间点或每天的某个时间点自行执行任务。而且 oracle 重新启动后,job 会继续运行,不用重新启动。

而且 job 的机制非常完备,可以查询相关的表或视图,查询 job 的定时规则和执行情况。缺点是作为 oracle 数据库层面的工具,自定义功能扩展,二次开发的难度比较大。

1.1 创建 job

DECLARE
  job NUMBER;
BEGIN
    sys.dbms_job.submit(job => job,

    what => 'prc_name;',                          -- 执行的存储过程的名字

    next_date => to_date('22-11-2013 09:09:41', 'dd-mm-yyyy hh24:mi:ss'), -- 下一次执行时间

    interval =>'sysdate+1/24');            -- 每天 24 小时,即每小时运行 prc_name 过程一次
END;

-- job 参数是输出参数,由 submit() 过程返回的 binary_ineger,这个值用来唯一标识一个工作。一般定义一个变量接收,可以去 user_jobs 视图查询 job 值。-- what 参数是将被执行的 PL/SQL 代码块,存储过程名称等。-- next_date 参数指识何时将运行这个工作。-- interval 参数何时这个工作将被重执行 

1.2 删除 job

DECLARE
BEGIN
  dbms_job.remove(1093);  -- 1093 为当前需要删除的 job 值
  COMMIT;
END;

1.3 查询 job

-- 查询当前用户的 job
select * from user_jobs;
-- 查询所有 job
select * from dba_jobs;
-- 查询所有运行中的 job
select * from dba_jobs_running;

2. crontab (linux)

crond 是 linux 下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与 windows 下的计划任务类似,当安装完成操作系统后,默认会安装此服务 工具,并且会自动启动 crond 进程,crond 进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。

cron 是服务名称,crond 是后台进程,crontab 则是定制好的计划任务表。大部分 linux 系统默认都安装了 cron,可以检查一下。

-- 检查 Crontab 工具是否安装
crontab -l
-- 检查 crond 服务是否启动
service crond status

-- centos 安装
yum install vixie-cron
yum install crontabs

crontab 基本操作命令

-- 列出某个用户 cron 服务的详细内容
crontab -l
-- 编辑某个用户的 cron 服务
crontab -e 

crontab 表达式格式

{minute} {hour} {day-of-month} {month} {day-of-week} {full-path-to-shell-script}

 minute: 区间为 0 – 59
 hour: 区间为 0 – 23
 day-of-month: 区间为 0 – 31
 month: 区间为 1 – 12. 1 是 1 月. 12 是 12 月
 Day-of-week: 区间为 0 – 7. 周日可以是 0 或 7.

在以上各个字段中,还可以使用以下特殊字符:星号(*):代表所有可能的值,例如 month 字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作。逗号(,):可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”中杠(-):可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”正斜线(/):可以用正斜线指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如 */10,如果用在 minute 字段,表示每十分钟执行一次。

推荐一个 crontab 表达式的校验网站(https://tool.lu/crontab/)

3. Timer 和 ScheduledExecutorService (java)

Timer 是 jdk 中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。

// 只执行一次
public void schedule(TimerTask task, long delay);
public void schedule(TimerTask task, Date time);
// 循环执行
// 在循环执行类别中根据循环时间间隔又可以分为两类
public void schedule(TimerTask task, long delay, long period) ;
public void schedule(TimerTask task, Date firstTime, long period) ;
 
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

TimerTask 是一个实现了 Runnable 接口的抽象类,代表一个可以被 Timer 执行的任务。TimerTask 类是一个抽象类,由 Timer 安排为一次执行或重复执行的任务。它有一个抽象方法 run() 方法,该方法用于执行相应计时器任务要执行的操作。因此每一个具体的任务类都必须继承 TimerTask,然后重写 run() 方法。
另外它还有两个非抽象的方法

-- 取消此计时器任务
boolean cancel()
-- 返回此任务最近实际执行的安排执行时间
long scheduledExecutionTime()

当然,一般使用 Timer 的比较少,因为它的缺点比较明显:

  1. 单线程,当多个 timer 同时运行时,会等上一个执行完成,再执行下一个。
  2. Timer 线程是不会捕获异常的,如果 TimerTask 抛出的了未检查异常则会导致 Timer 线程终止。

所以一般使用 ScheduledExecutorService 替代 Timer。
ScheduledExecutorService:也是 jdk 自带的一个基于线程池设计的定时任务类。其每个调度任务都会分配到线程池中的一个线程执行,所以其任务是并发执行的,互不影响。

4. SpringTask (spring)

Timer 和 ScheduledExecutorService 都是属于 jdk 层面上实现定时调度的类,功能还不足以让我们满意,那么现在介绍一个比较完善的定时调度工具 – SpringTask,是 Spring 提供的,支持注解和配置文件形式,支持 crontab 表达式,使用简单但功能强大。我个人非常喜欢 SpringTask,仅仅是因为支持 crontab 表达式。

在 springboot 里面使用方式非常简单:

  1. 启动类添加开启定时调度的注解 @EnableScheduling
  2. 在需要定时执行的方法上,增加注解 @Scheduled(cron =”crontab 表达式 ”)

默认的简单的使用步骤只有以上两步,但是 SpringTask 的默认使用方式也有一些不足:

  1. 默认线程池的 poolsize 为 1,可以理解为 Timer 类似的单线程模式。
  2. 无法动态修改 crontab 表达式,修改完只能重新部署后,才能生效。

问题 1 的解决方式,可以通过自定义 TaskExecutor 来修改当前的线程池。问题 2,则可以直接使用 threadPoolTaskScheduler 类实现自定义的定时调度规则。

附解决两个问题的源码 TaskTimer.class

@Component
public class TaskTimer {

    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    @Autowired
    private TaskRepo taskRepo;

    /**
     * *** 定时引擎 ***
     *
     * 实例化一个线程池任务调度类
     * 默认 ThreadPoolTaskScheduler 的 poolSize 为 1,类似于 newSingleThreadExecutor 单线程模式,只能执行完一个调度,再执行其他调度
     * 需要自定义扩展 poolSize,允许一定程度的 多线程并行场景
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(100);
        threadPoolTaskScheduler.setThreadNamePrefix("Thread -");
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        return threadPoolTaskScheduler;
    }

    /**
     * 启动定时调度
     * @param taskCode
     * @param cron
     * @param runnable
     * @return
     */
    public boolean start(String taskCode, String cron,Runnable runnable){ScheduledFuture<?> currentFuture=taskRepo.findTask(taskCode);
        // 已存在的调度,无法再创建
        if(currentFuture!=null){throw  new RuntimeException("调度 \""+taskCode+"\" 已存在,无法再创建 ");
        }
        // 创建新的调度,并加入 taskMap
        currentFuture = threadPoolTaskScheduler.schedule(runnable,new CronTrigger(cron));
        if (currentFuture!=null){this.taskRepo.addTask(taskCode,currentFuture);
            return true;
        }
        throw  new RuntimeException("任务启动失败!!!");
    }

    /**
     * 暂停定时调度
     * @param taskCode
     * @return
     */
    public boolean stop(String taskCode) {
        //taskId 不存在的,无法停止,只能修改
        ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        if(currentFuture!=null){return currentFuture.cancel(true);
        }
        return true;
    }

    /**
     * 删除定时调度
     * @param taskCode
     * @return
     */
    public boolean remove(String taskCode){ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        if(currentFuture!=null){currentFuture.cancel(true);
             taskRepo.removeTask(taskCode);
             return true;
        }
        return false;
    }

    /**
     * 修改定时调度
     * @param taskCode
     * @param cron
     * @param runnable
     * @return
     */
    public boolean update(String taskCode,String cron,Runnable runnable){ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        // 已存在的定时调度,先停止,再新增,并更新 新的 ScheduledFuture
        if(currentFuture!=null) {currentFuture.cancel(true);
        }
        currentFuture= threadPoolTaskScheduler.schedule(runnable,new CronTrigger(cron));
        if(currentFuture!=null){this.taskRepo.addTask(taskCode,currentFuture);
            return true;
        }
        return false;
    }

}

5. Quartz (其他产品)

Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。它是一个功能强大、十分成熟的重量级产品,还支持负载均衡,实现分布式调度。

不过,对于 Quartz 的安装你要多花点功夫了,从数据库要建哪些表,到应用程序该如何部署。对于这样一个庞大的产品,本篇文章就不附上它的使用说明书了。

参考文档

  • SpringBoot 中并发定时任务的实现、动态定时任务的实现

正文完
 0