背景
我们在业务开发中经常碰到需要定时触发的一些业务场景。比如我们系统的一些业务需要统计一天的流量数据,在每天凌晨发送邮件给相关负责人 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 表达式等方式。
- 每个任务都是一个线程,无线程池配置。
- 配置需要重启工程。
- 最上面其他功能都不支持。
总结
今天介绍了两个比较简单的方式,大家应该了解了其运行原理和它们存在的一些问题。后面我们继续介绍其他的定时任务的框架实现,看看其他的实现是怎么解决这些问题的,又提供了什么有用的功能。还是那句话,挑最适合自己的。如果今天介绍的这两种方式已经能满足你的业务场景,那么使用也是可以的。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于