概述
定时任务平台可以在后台自动检测数据并进行操作。主要应用在订单状态改变、后台统计、定时发送邮件或短信等
传统的定时任务调度
可以通过Timertask、可定时线程池、Quartz、spring-schedule方式来进行处理。相关依赖和代码如下面所示
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/>
</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>
TimerTask用法
import java.util.Timer;
import java.util.TimerTask;
public class TaskscheduleTest {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "TimerTask-定时任务触发");
}
};
Timer timer = new Timer();
long delay = 0;// 天数
long period = 1000;// 毫秒数
timer.scheduleAtFixedRate(timerTask, delay, period);
}
}
线程池定时任务
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println("线程池-定时任务触发..");
}
};
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 2, TimeUnit.SECONDS);
}
Quartz框架任务调度
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();
}
}
spring-schedule定时任务调度
@Component
public class UserScheduled {
@Scheduled(cron = "0/1 * * * * *")
public void taskUserScheduled() {
System.out.println("定时任务触发...");
}
}
传统的定时任务方式对比
spring-schedule方式可以减少代码量,不过需要依赖spring相关的注解。
可定时任务线程池可以完全脱离spring注解来执行定时任务。
quartz的代码相对繁琐些。
同时他们底层的原理都是死循环来进行实现的
传统定时任务不足
- 没有补偿机制,一旦出现异常,那么就不会执行,需要等到第二次的任务才能执行。
- 不支持集群,不支持路由策略
- 没有管理平台,无法做统计
- 没有报警邮箱,状态监控
- 业务逻辑与定时任务逻辑放入在同一个Jar包中,如果定时任务逻辑挂了也会影响到业务逻辑,即没有实现解耦
分布式定时任务框架XXL-job
详细步骤可参考官网:分布式任务调度平台XXL-JOB
系统组成
- 调度中心:将执行器和任务注册至调度中心,由调度中心根据配置发出调度请求。先配置执行器,然后创建任务,因为任务是由执行器统一管理。
- 执行器:负责接收调度请求的服务器,接收之后调用JobHandler去执行任务逻辑,执行器在启动的时候会将他的IP和端口号信息注册到调度中心
- 任务:JobHandler,即是要实现的具体业务代码
注意:中心式调度器和分布式执行器在使用时需要分别启动,在调度中心启动时需要配置所依赖的 db 配置
项目整合XXL-job任务调度平台+java实现定时任务实践
<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>
JobConfig-项目相关的执行器服务Config配置
@Configuration
public class demoJobConfig {
private Logger logger = LoggerFactory.getLogger(demoJobConfig.class);
@Value("${demo.job.admin.addresses}")
private String adminAddresses;
@Value("${demo.job.executor.appname}")
private String appName;
@Value("${demo.job.executor.ip}")
private String ip;
@Value("${demo.job.executor.port}")
private int port;
@Value("${demo.job.accessToken}")
private String accessToken;
@Value("${demo.job.executor.logpath}")
private String logPath;
@Value("${demo.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean(initMethod = "start", destroyMethod = "destroy")
public demoJobSpringExecutor demoJobExecutor() {
logger.info(">>>>>>>>>>> demo-job-executor config init.");
demoJobSpringExecutor demoJobSpringExecutor = new demoJobSpringExecutor();
demoJobSpringExecutor.setAdminAddresses(adminAddresses);
demoJobSpringExecutor.setAppName(appName);
if(ip == null || ip.isEmpty()) {
// 如果没有指定IP,則使用Spring cloud 获取当前的IP
InetUtils inetUtils = new InetUtils(new InetUtilsProperties());
ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
inetUtils.close();
}
logger.info("current job executor ip: " + ip);
demoJobSpringExecutor.setIp(ip);
demoJobSpringExecutor.setPort(port);
demoJobSpringExecutor.setAccessToken(accessToken);
demoJobSpringExecutor.setLogPath(logPath);
demoJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return demoJobSpringExecutor;
}
}
项目中具体定时任务DemoJob实现
@JobHandler(value = "DemoJob")
@Component
public class DemoJob extends IJobHandler {
private static Logger logger = LoggerFactory.getLogger(DemoJob.class);
@Autowired
private EmailTemplateService emailTemplateService;
@Autowired
private MailSendRecordService mailSendRecordService;
@Override
public void execute(String param) throws Exception {
//具体的任务实现
......
System.out.print("定时任务发送成功");
}
}
扩展
如果在应用集群情况下,分布式job怎么解决幂等性?
- 使用分布式锁,(ZK,redis)保证只有一台服务器在执行定时job。
- 使用配置文件,配置文件上加上开关,flag=true的情况下才执行,false不执行。打包的时候,打不一样的配置文件的包。
- 使用数据库的唯一标识,谁插入进去,谁就执行,但是效率低,一般不会采用。