定时任务系列之 crontab&Timer 实现

本贴最后更新于 1912 天前,其中的信息可能已经事过境迁

背景

我们在业务开发中经常碰到需要定时触发的一些业务场景。比如我们系统的一些业务需要统计一天的流量数据,在每天凌晨发送邮件给相关负责人 XX。或者,每个月底要发送报表给 XX。
人工实现是不可能的,我是不会凌晨爬起来给你手动触发事件的 ; )
所以我们需要一个定时任务处理框架。你有两个选择:选择开源的成熟框架或其他已有的轮子;现有框架满足不了,自己造轮子。
但我觉得大部分人对定时任务的要求都没有那么高... 我们还是要考虑开发成本的嘛,所以我们大可以选择现成的定时任务实现就可以啦。
我们在这一系列中会介绍 shell、spring scheduler、quartz、timer、xxl-job 等,会有相关代码实现以及部分的源码解读。

如何选择一个定时任务框架

这个问题是最重要的。选择一个技术不是因为这个技术好,而是适合你的业务场景
我们选择定时任务框架的时候可以根据几个简单的标准【标准可以根据业务适当调整】

  • 易于配置,低耦合。
  • 占用资源少,性能好。
  • 能配置失败策略,如重试 or 通知。
  • 支持分布式集群调度。
  • 易于扩展。
  • 执行记录可视化。
  • 支持 kill 正在执行的任务。
  • 有可视化界面。

crontab

如果你是 linux 系统,very good! 我们可以通过很简单的方式实现定时任务的配置和调用。
linux 带有一个 crontab 的命令。这个命令如何使用呢?建议大家可以先使用 man 命令查下具体参数,下面给大家展示示例。

优点
  • 简单粗暴。
  • 支持 cron 表达式。
  • 支持实时修改删除配置。

缺点

  • 过于简单,导致其他的定时模式不支持。
  • cron 表达式支持尺度只能到分钟级别。
  • 在生产中可能会导致需要其他角色(运维)参与业务。
  • 不支持分布式调度等。【参考上面的标准】

应用场景

适用于那种调度频率不是很频繁、执行失败可以容忍的定时任务。
需要将自己的代码写的尽量健壮。

Timer

Timer 是 Java 提供的一个定时任务工具。
我们可以通过代码来看看 Timer 是如何处理定时任务的。

// 声明一个Timer对象
Timer timer = new Timer();
// 调用schedule,传入task(继承Runnable),和需要触发任务的时间,此处设置为当前时间的5s后。
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        log.warn("timer --------- timer");
    }
}, new Date(new Date().getTime() + 5000));

Timer 内部维护了一个 TimeTask 的数组,通过对添加的任务进行触发时间排序,挨个来触发。对于多线程的并发操作,内部采用了 sync 关键字的方式解决。简单看下面代码

    private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");

        // 防止值太大
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        // 加锁操作,因为要添加任务到queue了。
        synchronized (queue) {
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");
            // 根据传入的参数,处理task
            synchronized (task.lock) {
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                            "Task already scheduled or cancelled");
                // 设置的触发时间被赋给task
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }
            // 此处是添加操作,涉及到数组的copy & 任务排序等
            queue.add(task);
            if (queue.getMin() == task)
                queue.notify();
        }
    }

    // queue的添加方法
    void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
	// 处理排序
        fixUp(size);
    }

    // 处理触发时间的排序
    private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

优点

  • Timer 的方式与上面相比至少不需要其他角色参与
  • Timer 的方式比较简单。
    ps : 感觉在强行扯...

缺点

Timer 的缺点比起优点来看,好像多了很多。比如没有可视化界面这种我们先不提,具体有以下几个

  • 只能支持传入触发时间和周期。不支持 cron 表达式等方式。
  • 每个任务都是一个线程,无线程池配置。
  • 配置需要重启工程。
  • 最上面其他功能都不支持。

总结

今天介绍了两个比较简单的方式,大家应该了解了其运行原理和它们存在的一些问题。后面我们继续介绍其他的定时任务的框架实现,看看其他的实现是怎么解决这些问题的,又提供了什么有用的功能。还是那句话,挑最适合自己的。如果今天介绍的这两种方式已经能满足你的业务场景,那么使用也是可以的。


  • 定时任务
    14 引用 • 27 回帖
  • cron
    11 引用 • 3 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...