相信定时任务大家都不陌生,可能在个人项目中使用定时任务会比较少,但是在业务复杂的公司中,定时任务是不可或缺的一部分,有时候也可以贯穿整个项目的流程运转。

什么是定时任务

crond类似于我们平时生活中的闹钟,可以定时叫你起床,而在项目中就是定时执行一段指定的代码

为什么要使用crond呢

1.如果对于一些数据实时性要求没那么高,我们可以把数据提前丢到缓存中,这个时候就需要使用定时任务去跑了,比如每天凌晨3点定时把数据同步到缓存,错峰同步避开白天人流量大的时候消耗资源

2.比如凌晨2点有抢购接口/或者业务开关需要进行变更开启,我们可以使用定时任务去进行变更,不用人为去守着变更,而且执行时间更准确(可以滚去睡大觉.jpg)

3.还可以进行数据的定时备份,比如备份配置文件,防止宕机的时候配置文件的恢复等等

定时任务实现方式

1.Thread

各位亲爱的朋友,没错,Thread真的可以做定时任务.

如果你去看过一些定时任务框架的源码,它们的底层也会使用Thread类(需要注意用try……catch捕获异常,否则出现异常,就直接退出循环)

1
2
3
4
5
6
7
8
9
10
11
12
public static void init() {
new Thread(() -> {
while (true) {
try {
System.out.println("doSameThing");
Thread.sleep(1000 * 60 * 5);
} catch (Exception e) {
log.error(e);
}
}
}).start();
}

使用场景:比如项目中有时需要每隔10分钟去下载某个文件,或者每隔5分钟去读取模板文件生成静态html页面等等,一些简单的周期性任务场景。

优缺点:

  • 优点:这种定时任务非常简单,学习成本低,容易入手,对于那些简单的周期性任务,是个不错的选择。
  • 缺点:不支持指定某个时间点执行任务,不支持延迟执行等操作,功能过于单一,无法应对一些较为复杂的场景。

2.Timer

Timer 类是jdk专门提供的定时器工具,用来在后台线程计划执行指定任务,在 java.util 包下,要跟 TimerTask 一起配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void Timer(){
Timer timer = new Timer();
// 1.需要执行的内容
// 2.延时多久执行
// 3.执行周期
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(123);
}
},200,1000);
}

优缺点:

  • 优点:非常方便实现多个周期性的定时任务,并且支持延迟执行,还支持在指定时间之后支持,功能还算强大。
  • 缺点:如果其中一个任务耗时非常长,会影响其他任务的执行。并且如果 TimerTask 抛出 RuntimeException , Timer 会停止所有任务的运行,所以阿里巴巴开发者规范中不建议使用它

3.Scheduled 注解(常用)

由于xml方式太古老了,我们以springboot项目中注解方式为例

1.引入依赖

1
2
3
4
5
6
7
<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-context</artifactId>

</dependency>

2.在springboot启动类上加上 @EnableScheduling 注解

1
2
3
4
5
6
@EnableScheduling
@SpringBootApplication
public class Application {
……
}

3.使用 @Scheduled 注解定义定时规则

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class CronJob {
@Scheduled(cron = "0/5 * * * * ?")
public void autoGetMessage(){
System.out.println("当期执行时间:" + new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss").format(new Date()));
}
}

优缺点:

  • 优点:采用cron表达式,比较方便,spring框架自带的定时功能,springboot做了非常好的封装,开启和定义定时任务非常容易,可以满足绝大多数单机版的业务场景。单个任务时,当前次的调度完成后,再执行下一次任务调度。
  • 默认单线程,如果前面的任务执行时间太长,对后面任务的执行有影响。不支持集群方式部署,不能做数据存储型定时任务。

5.spring quartz(常用)

quartz 是 OpenSymphony 开源组织在 Job scheduling 领域的开源项目,是由java开发的一个开源的任务日程管理系统。

quartz能做什么?

  • 作业调度:调用各种框架的作业脚本,例如shell,hive等。
  • 定时任务:在某一预定的时刻,执行你想要执行的任务

1.引入相关依赖

1
2
3
4
5
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>

2.定时任务执行类继承QuartzJobBean

1
2
3
4
5
6
7
8
public class DateTimeJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//获取JobDetail中关联的数据
String msg = (String) jobExecutionContext.getJobDetail().getJobDataMap().get("msg");
System.out.println("current time :"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---" + msg);
}
}

3.调度配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class QuartzConfig {
@Bean
public JobDetail printTimeJobDetail(){
return JobBuilder.newJob(DateTimeJob.class)//PrintTimeJob我们的业务类
.withIdentity("DateTimeJob")//可以给该JobDetail起一个id
//每个JobDetail内都有一个Map,包含了关联到这个Job的数据,在Job类中可以通过context获取
.usingJobData("msg", "Hello Quartz")//关联键值对
.storeDurably()//即使没有Trigger关联时,也不需要删除该JobDetail
.build();
}
@Bean
public Trigger printTimeJobTrigger() {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
return TriggerBuilder.newTrigger()
.forJob(printTimeJobDetail())//关联上述的JobDetail
.withIdentity("quartzTaskService")//给Trigger起个名字
.withSchedule(cronScheduleBuilder)
.build();
}
}

优缺点:

  • 优点:默认是多线程异步执行,单个任务时,在上一个调度未完成时,下一个调度时间到时,会另起一个线程开始新的调度,多个任务之间互不影响。支持复杂的 cron 表达式,它能被集群实例化,支持分布式部署
  • 缺点:相对于spring task实现定时任务成本更高,需要手动配置 QuartzJobBean 、 JobDetail和 Trigger 等。需要引入了第三方的 quartz 包,有一定的学习成本。不支持并行调度,不支持失败处理策略和动态分片的策略等。

以下两种配置参考方式

springXML配置方式

SpringBoot配置方式

6.xxl-job(常用:分布式定时任务主流)

xxl-job 是大众点评(许雪里)开发的一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用

xxl-job 框架对 quartz 进行了扩展,使用 mysql 数据库存储数据,并且内置jetty作为 RPC服务调用。

具体配置可以参考之前本人写的【初探xxljob

优缺点:

  • 优点:有界面管理定时任务,支持弹性扩容缩容、动态分片、故障转移、失败报警等功能。它的功能非常强大,很多大厂在用,可以满足绝大多数业务场景。
  • 缺点:和 quartz 一样,通过数据库分布式锁,来控制任务不能重复执行。在任务非常多的情况下,有一些性能问题。

分布式定时任务

在前面提到Timer/ScheduledExecutorService/SpringTask(@Schedule)都是单机的,但我们一旦上了生产环境,应用部署往往都是集群模式的。

在集群下,我们一般是希望某个定时任务只在某台机器上执行,那这时候,单机实现的定时任务就不太好处理了。

Quartz是有集群部署方案的,所以有的人会利用数据库行锁或者使用Redis分布式锁来自己实现定时任务跑在某一台应用机器上;做肯定是能做的,包括有些挺出名的分布式定时任务框架也是这样做的,能解决问题。

但我们遇到的问题不单单只有这些,比如我想要支持容错功能(失败重试)、分片功能、手动触发一次任务、有一个比较好的管理定时任务的后台界面路由负载均衡等等。这些功能,就是作为「分布式定时任务框架」所具备的。

分布式定时任务框架又可以分成了两个流派:中心化和去中心化

  • 所谓的「中心化」指的是:调度器和执行器分离,调度器统一进行调度,通知执行器去执行定时任务
  • 所谓的「去中心化」指的是:调度器和执行器耦合,自己调度自己执行

对于「中心化」流派来说,存储相关的信息很可能是在数据库(DataBase),而我们引入的client包实际上就是执行器相关的代码。调度器实现了任务调度的逻辑,远程调用执行器触发对应的逻辑。

img

谈谈定时任务使用场景

我现在公司对于定时任务的主要使用场景是更新缓存,清洗数据,定时推送,定时拉单等等

先说说更新缓存:就是每隔一段时间去执行操作,可能一天只需要更新一次当天的缓存数据,一般会选择放在凌晨人流量少的时候执行。

清洗数据和定时拉单就比较像,会比如每间隔15分钟去进行一次扫表,看看有没有需要进行执行的流程

定时推送就是你想的那样,定时推送消息或者定时执行代码的开关

对于以上的操作,我们可以分为两种,全量更新和增量更新

全量更新:这个比较常用,就是一次性全部查询出来,然后一次性更新到缓存里面去

1
2
3
4
5
6
7
8
9
10
11
12
public void zxMachineBuild(){
// 1.查询需要缓存的数据
List<Tax> taxs = tConfigPolicyMapper.getTConfigTax();
// 2.拼接存储格式
Map<String,Object> map = new HashMap<>();
for(Tax tax:taxs){
……
map.put(tax.getPindex(),tax);
}
// 3.更新到redis
redisService.batchPutInPipelined(map,60*60*24);
}

注意:因为以上使用的是有过期时间的,如果正常跑的话一般会先删后增,如果是redis的话可以直接全部覆盖(使用场景一般是初始化的时候或者数据重跑)

增量更新:增量的基础就是全量,先全量更新后,再用增量方式同步更新,一般利用节点或者状态去进行更新

我们常用的更新方式

根据状态更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void zxMachineBuild(String type){
// 1.查询需要增量的数据
List<Tax> taxs = tConfigPolicyMapper.getTConfigTax(type);
// 2.拼接存储格式
Map<String,Object> map = new HashMap<>();
for(Tax tax:taxs){
……
map.put(tax.getPindex(),tax);
}
// 3.更新到redis,根据数据判断使用过期还是精准删除
redisService.batchPutInPipelined(map,60*60*24);
// 4.更新状态
tConfigPolicyMapper.updateTConfigType(taxs);
}

根据节点更新:假设我使用redis来存储我的节点(一般会使用id作为节点,以下例子使用时间节点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void zxMachineBuild(){
// 获取上次更新的时间节点
long nodeTime = redisService.getNodeTime();
// 获取在该时间节点之后的数据(根据时间排序)
List<Tax> taxs = tConfigPolicyMapper.getTConfigTax(nodeTime);
// 拼接存储格式
Map<String,Object> map = new HashMap<>();
for(Tax tax:taxs){
……
map.put(tax.getPindex(),tax);
// 如果不根据时间排序就比较获取
if(tax.getCreateTime() != null && tax.getCreateTime() > nodeTime){
nodeTime = tax.getCreateTime();
}
}
// 3.更新到redis,根据数据判断使用过期还是精准删除
redisService.batchPutInPipelined(map,60*60*24);
// 4.更新时间节点到redis(获取最后一个的时间节点)
redisService.updateNodeTime(nodeTime);
}

进阶(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void zxMachineBuild(){
// 随机一个uuid
String uuid = UUID.randomUUID().toString().replaceAll("-","");
// 锁定
tConfigPolicyMapper.Lock(uuid);
// 查询提前锁定好的数据
List<Tax> taxs = tConfigPolicyMapper.getTConfigTax(uuid);
// 拼接存储格式
Map<String,Object> map = new HashMap<>();
for(Tax tax:taxs){
……
map.put(tax.getPindex(),tax);
// 如果不根据时间排序就比较获取
if(tax.getCreateTime() != null && tax.getCreateTime() > nodeTime){
nodeTime = tax.getCreateTime();
}
}
// 更新到redis,根据数据判断使用过期还是精准删除
redisService.batchPutInPipelined(map,60*60*24);
// 更新锁定状态为已跑
tConfigPolicyMapper.updateNodeTime(taxs);
}

使用以上方式的好处就是可以直接先锁定自己需要跑的部分,防止其他的定时任务抢占

1
2
3
4
5
## 取t_pao_lowprice_taobao数据出来,设置state=‘L’ 锁住
update [t_pao_lowprice_taobao] set lockname='guid',locktime=GETDATE(),state='L'
from [t_pao_lowprice_taobao] a join
(select top 2 id from [t_pao_lowprice_taobao] where lockname is null and state='N' order by createtime asc)
b on a.id=b.id
1
2
## lockname【对应代码的uuid】 状态为state=‘L’ 获取需要跑的数据,开线程去跑
select * from [t_pao_lowprice_taobao] where lockname='guid' and state='L'

总结

可能在个人项目中,对于定时任务的需求没有那么多,不会去重视这一块,但是在实际工作中,定时任务往往扮演着重要的角色

不同的定时任务有不同的优缺点,往往我们去选择适合自己的那种方式,就需要对于其种类有一定的了解,怎样才能更高效的去进行开发,尽量避免使用到一半因为其底层设计的原因导致BUG。

现在基本也使用E-Job(ElasticJob)或者X-Job(XXLJob)这种分布式定时任务,它们都有广泛的用户基础和完整的技术文档

  • X-Job 侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在“用户基数相对少,服务器数量在一定范围内”的情景下使用
  • E-Job 关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在“数据量庞大,且部署服务器数量较多”时使用

单机的定时任务现在基本不推荐使用了,维护起来比较麻烦

对于定时任务,个人理解程度有限,使用的场景往往更加复杂不能每点都考虑到,欢迎大家能提出自己的想法

比如分布式定时任务下如何去保证事务的一致性,单机定时任务又如何去保证数据不重复等等场景都需要去挖掘……

参考指南: