还没真正的遇到使用定时任务的场景,不管怎么说先学起来
1. 定时任务
很多情况下任务并非需要立即执行,而是需要往后或定期执行,这不可能人工去操作,所以定时任务就出现了。项目中肯定会用到使用定时任务的情况,笔者就需要定时去拉取埋点数据
使用定时任务的情况:
- 每周末凌晨备份数据
- 触发条件 5 分钟后发送邮件通知
- 30 分钟未支付取消订单
- 每 1 小时去拉取数据
- ......
2. Thread实现
笔试中首次遇到定时任务急急忙忙想出来的方法
2.1 使用
public class ThreadSchedule {
public static void main(String[] args) {
// 5 秒后执行任务
int interval = 1000 * 5;
// 新线程执行
new Thread(() -> {
try {
Thread.sleep(interval);
System.out.println("执行定时任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
2.2 分析
- 定时不准确,因依赖底层硬件,Windows误差为10微妙
- System.currentTimeMillis() 依赖系统硬件,还会受网络时间同步修改
- System.nanoTime() 依赖 JVM 的运行纳秒数,并不受同步影响,适用于计算准确的时间差
- 但计算当前日期还是要使用 currentTimeMillis 的格林威治时间,而 nanoTime 计算 JVM 运行时间不准确
3. java.util.Timer
Timer 负责执行计划功能,会启动一个后台线程,而 TimerTask 负责任务逻辑
3.1 使用
public class TimeSchedule {
public static void main(String[] args) {
// 启动一个守护线程
Timer timer = new Timer();
// 定时任务
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务");
}
};
long delay = 0; // 延迟执行
long period = 1000 * 5; // 间隔
timer.scheduleAtFixedRate(timerTask, 1, period);
// timer.cancel(); 可取消
}
}
3.2 分析
// new Timer() 最终的构造函数会启动一个线程
public Timer(String name) {
thread.setName(name);
thread.start();
}
// 这个线程里面封装了一个 Queue 优先级队列,该线程会去队列里不停执行里面的任务
class TimerThread extends Thread {
private TaskQueue queue;
TimerThread(TaskQueue queue) {
this.queue = queue;
}
}
// 这个队列里面存放了各种 TimerTask 定时的任务逻辑
class TaskQueue {
private TimerTask[] queue = new TimerTask[128];
}
- 只有一个单线程执行,所以是串行执行
- 某个任务执行时间较长会阻塞后面预定执行的任务,所以时间并不准确
- 线程报错后续的定时任务直接停止
4. ScheduledExecutorService
java.util.concurrent中的工具类,是一个多线程的定时器
4.1 使用
public class ExecutorSchedule {
public static void main(String[] args) {
// 定时任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("执行定时任务");
}
};
// 线程池执行
long delay = 0; // 延迟执行
long period = 1000 * 5; // 间隔
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate(runnable, delay, period, TimeUnit.MILLISECONDS);
}
笔者最常用的一个定时操作了,之前还写过定时的探测任务
5. Spring的定时任务
需要开启定时功能@EnableScheduling
@Component
public class SpringSchedule {
// cron 表达式,每秒执行一次
@Scheduled(cron = "*/1 * * * * ?")
public void springSchedule(){
System.out.println("执行定时任务");
}
}
底层是 ScheduledThreadPoolExecutor 线程池,和上面的 ScheduledExecutorService 是同根同源
6. XXL-JOB
xxl-job 是个人维护的分布式任务调度框架(国人写的,有详细的中文文档),分为 调度中心 和 执行器。执行器就是定时任务,而调度中心则负责管理调用这些定时任务,调度中心也可以存储定时任务通过脚本形式(Java 是 Grovvy)免编译地实时下发到各服务中执行。最重要的是有 UI 界面,用户友好的体验
6.1 建立数据库
xxl-job 的存储是基于数据库的,相对比 quartz 可保存在内存和数据库有一点性能影响。首先第一步就是要建库,在 xxl-job 官网有 SQL 语句 tables_xxl_job.sql,直接执行即可建库建表
6.2 部署 xxl-job-admin 调度中心
从 Git 上拉取最新的代码,然后编译根模块,填好 admin 模块的数据库地址等,即可启动这个调度中心(支持 Docker 部署,更加方便)
6.3 创建定时任务
在需要定时任务的服务中 引入依赖、添加配置、创建定时任务
6.3.1 依赖
com.xuxueli
xxl-job-core
${project.parent.version}
6.3.2 基本配置
# xxl-job admin address
xxl.job.admin.addresses=http://xxx.xxx.xxx:8080/xxl-job-admin
# xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-demo
6.3.3 定时任务
@Component
public class MyJob {
@XxlJob("MyJob")
public void MyJob() throws Exception {
// 执行器日志记录
XxlJobHelper.log("myjob is execute");
// 定时任务逻辑
System.out.println("myjob is executing");
// default success
}
}
6.4 执行定时任务
进入调度中心新建一个任务,然后执行定时任务即可(使用的是 RPC 远程过程调用)
6.5 遇到的问题
默认执行器是自动注册到调度中心的,但是时常进去的地址有问题而导致执行失败,所以要手动录入执行器的地址
6.6 分析
作为轻量级的分布式定时任务,有 UI 界面简单方便使用,而且对代码没什么侵入性,已经能满足大部分项目的需求了,笔者如果要用定时任务也会首选 xxl-job