为什么用定时任务?
定时任务平台可以在后台自动检测数据并进行操作。主要应用在订单状态改变、后台统计、定时发送邮件或短信等。
定时任务怎么部署实现?
传统的定时任务可以通过可定时线程池、timertask、quartz、spring-schedule方式来进行处理。他们的依赖和代码如下面所示。
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
代码实现
timetask法
public class TimerTaskDemo {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "定时任务触发");
}
};
Timer timer = new Timer();
// 天数
long delay = 0;
// 耗秒数
long period = 1000;
timer.scheduleAtFixedRate(timerTask, delay, period);
}
}
可定时线程池法
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println("定时任务触发..");
}
};
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS);
}
}
quartz法
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
//1.创建Scheduler的工厂
SchedulerFactory sf = new StdSchedulerFactory();
//2.从工厂中获取调度器实例
Scheduler scheduler = sf.getScheduler();
//3.创建JobDetail
JobDetail jb = JobBuilder.newJob(MyJob.class)
.withDescription("this is a ram job") //job的描述
.withIdentity("ramJob", "ramGroup") //job 的name和group
.build();
//任务运行的时间,SimpleSchedle类型触发器有效
long time = System.currentTimeMillis() + 3 * 1000L; //3秒后启动任务
Date statTime = new Date(time);
//4.创建Trigger
//使用SimpleScheduleBuilder或者CronScheduleBuilder
Trigger t = TriggerBuilder.newTrigger()
.withDescription("")
.withIdentity("ramTrigger", "ramTriggerGroup")
//.withSchedule(SimpleScheduleBuilder.simpleSchedule())
.startAt(statTime) //默认当前时间启动
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) //两秒执行一次
.build();
//5.注册任务和定时器
scheduler.scheduleJob(jb, t);
//6.启动 调度器
scheduler.start();
}
}
public class MyJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("quartz MyJob date:" + new Date().getTime());
}
}
spring-schedule法
@Component
public class UserScheduled {
@Scheduled(cron = "0/1 * * * * *")
public void taskUserScheduled() {
System.out.println("定时任务触发...");
}
}
从以上的代码可以看出,使用spring-schedule法可以减少代码量,只不过依赖于spring相关的注解。可定时任务线程池可以完全脱离spring注解来执行定时任务。quartz的代码相对繁琐些。同时他们底层的原理都是死循环来进行实现的。
传统定时任务的局限性
问题来了,随着数据量和用户数量的增加,传统定时任务的局限性日渐凸显。比如说,笔者在最早的公司深绘工作时,遇上如下两个问题:第一个问题是定时任务与普通业务逻辑代码耦合在一个项目里面发布,一旦定时任务过多,就会引起CPU飚高问题,造成整个系统的崩溃,代表的失误是用户运维统计数目过多导致系统的崩溃;第二个问题是,在集群的情况之下,多台服务器执行同一个定时任务,造成统计数据重复的问题,专业的角度来讲是幂等性问题,典型的代表是ai复核统计数据重复的问题。
从上面提到的问题中,可以概况出传统定时任务的局限性如下。
1.业务逻辑与定时任务逻辑放入在同一个Jar包中,如果定时任务逻辑挂了也会影响到业务逻辑;—没有实现解耦
2.如果服务器集群的情况下,可能存在定时任务逻辑会重复触发执行;
3.定时任务执行非常消耗cpu的资源,可能会影响到业务线程的执行
如何解决以上三点问题呢?
针对重复执行问题,笔者采用的第一个方法是加入联合唯一键来限制,从而保证唯一数据。第二个方法是使用分布式锁来进行处理,只要jar能够拿到分布式锁就能够执行定时任务,否则情况下不执行。第三个方法是对Jar包加上一个开关,项目启动的时候读取该开关 如果为true的情况下则加载定时任务类,否则情况下就不加载该定时任务类。以上三个方法都没有发挥集群优势,效率偏低。
针对定时任务挂了影响正常逻辑,笔者所在的深绘后来采用用户运维统计业务放到另外一个项目来进行处理,把定时任务和业务逻辑分开实现解耦、只对业务逻辑实现集群,不对我们的定时任务逻辑集群,这个方法不能够发挥集群的优势处理。
针对cpu飚高问题,笔者采用分片进行统计,每统计一次对线程休眠,防止CPU飚高和内存溢出的问题。
总而言之,针对传统定时任务框架的局限性,这里需要防止cpu飚高和发挥集群的优势,需要一个可控制的任务调度平台来处理。常见的定时任务调度平台有xxl-job和elastic-job。前者是利用自定义的admin调度平台来进行调度执行任务的,任务数据是放入数据库里面的;后者是用zookeeper来进行调度执行的。
分布式定时任务原理
1.执行器启动的时候会将他的IP和端口号信息注册到执行器注册中上。
2.当我们现在定时任务模块中启动定时任务的时候,定时任务会再admin
项目先触发,会根据执行器的名称查询多个不同的执行的ip信息,在采用
负载均衡算法选择一个地址,发送rest请求通知给执行器执行到定时任务。
项目如何整合xxl-job
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
java代码
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
@Component
public class SampleXxlJob {
private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);
/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
// default success
}
}