什么是定时任务?
定时任务是指在预定的时间间隔或特定时间点执行的任务或操作。这些任务通常用于自动化处理重复性、周期性的工作,减轻人工干预的负担,提高效率和准确性。最好的例子就是闹钟,你提前定好时间,他到了时间会自动触发。还有一种是根据时间间隔来的,依旧是闹钟,我第一次关闭之后,他会每隔10分钟后再启动。
定时任务和延迟任务
我个人认为定时任务和延迟任务是有一定区别的,定时任务是在预定的时间间隔或特定时间点执行的任务。也就是说他是事先规定好的。
延迟任务是在一定的延迟时间后执行的任务,即任务会在设定的延迟时间过后开始执行。一般是用户进行某个操作后一段时间执行。也就是说,延时任务是后天触发的,并不是提前设定好的。
当然,通过定时任务,也可以去实现延迟任务,最简单的方法就是轮询,当达到某个条件后执行任务。
常见的实现定时任务的技术栈
1.Timer
import java.util.Timer;
import java.util.TimerTask;
public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("定时任务执行:当前时间为 " + System.currentTimeMillis());
}
};
// 在延迟0毫秒后开始执行任务,然后每隔1秒执行一次
timer.schedule(task, 0, 1000);
}
}
优点:简单,易操作,不需要依赖其它中间件
缺点:无法适用于高并发和分布式场景,有多个任务同时添加时前面的会影响后面的任务
2.ScheduledExecutorService
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("定时任务执行:当前时间为 " + System.currentTimeMillis());
}
};
// 在延迟0秒后开始执行,每隔2秒执行一次
executor.scheduleAtFixedRate(task, 0, 2, TimeUnit.SECONDS);
// 等待一段时间后关闭 ScheduledExecutorService
try {
Thread.sleep(10000); // 等待10秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭 ScheduledExecutorService
executor.shutdown();
}
}
优点: 解决了上述Timer的并发问题
缺点:只能在单机环境使用
3.springTask
首先,确保在 Spring Boot 应用程序中添加以下依赖:
<dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
@SpringBootApplication
@EnableScheduling //开启定时任务
public class TestApplication {
}
在启动类似添加注解表示开启定时任务
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTasks {
@Scheduled(cron = "0 0/5 * * * ?") // 每隔5分钟执行一次,整点触发
public void runTask() {
System.out.println("定时任务执行:当前时间为 " + System.currentTimeMillis());
}
}
cron表达式大家可以自己去了解一下
4.quarz
Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:
- 通过调用API的方式操作任务,不人性化。
- 需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
- 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。
- 配置复杂
5.es-job
以quarz为基础,使用zookeeper做协调,调度中心的一个分布式定时任务框架,支持任务的分片,提供运维中心,使用相对较广,但需要耦合zookeeper,配置繁琐,
6.xxl-job
终于到今天的主角,
xxl-job是一个分布式的任务调度平台,其核心设计目标是:学习简单、开发迅速、轻量级、易扩展,现在已经开放源代码并接入多家公司的线上产品线,开箱即用。由国内开发大佬许雪里开发。
官网:分布式任务调度平台XXL-JOB (xuxueli.com)
更多细节和架构的东西,大家自行去官网查看;虽然xxl-job拥有如此多的优点,但它无法直接被服务端进行调用,必须通过注册中心才能对任务进行配置;个人感觉对于有些场景不是很友好,毕竟不一定是开发者才能去用任务调度相关功能;下面给大家演示一下个人服务如何远程连接xxl-job
实战
基本使用
首先在官网将项目拉到本地,如果是想在服务器上跑可以用docker直接部署到服务器
xxl-job-admin模块对应一个admin,也就是管理台或者称为调度中心,管理台可以设置成多个模块,使用不同的端口,但是这样没有意义,一般项目中就一个admin模块。
xxl-job-core是公共的核心模块。
xxl-job-executor-samples模块下存放的是各个执行器模块,每个执行器模块可以看成是一个单独的服务,执行器是绑定到管理台模块下的,通过在配置文件中的xxl.job.admin.addresses属性。
我们随便进入到一个执行器模块中,看一下任务是如何写的。也可以看作是不同的客户端服务
记得将数据库配置改成自己的
这个就相当于自己的服务,需要将管理中心地址配置上,同时配置自己数据库
admin依赖的表在doc文件里,直接引入配置到admin即可
启动服务成功
登陆管理中心
admin
123456
远程调用
依赖:
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- spring-boot-starter-web (spring-webmvc + tomcat) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.14</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.26</version>
</dependency>
核心类:
package com.xxl.job.executor.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xxl.job.executor.configProperties.XxlJobClientConfigProperties;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @Author Larry
* @Date 2024/1/31 11:12
* 实现远程调用
*/
@Component
public class XxlJobClient {
private String COOKIE ="";
private HttpClient httpClient;
private static final String POST_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8";
private static final Header POST_FORM_CONTENT_TYPE_HEADER = new Header("Content-Type",POST_FORM_CONTENT_TYPE);
@Autowired
private XxlJobClientConfigProperties clientConfigProperties;
private final Logger log = LoggerFactory.getLogger(getClass());
@PostConstruct
public void init() throws IOException {
log.debug("xxl JOB 初始化配置:{}",clientConfigProperties.toString());
httpClient = new HttpClient();
login();
}
/**
* 登录获取 cookie
* @throws IOException
*/
public void login() throws IOException {
HttpMethod postMethod = new PostMethod(clientConfigProperties.getLoginUrl());
httpClient.executeMethod(postMethod);
if (postMethod.getStatusCode() == 200) {
Cookie[] cookies = httpClient.getState().getCookies();
StringBuilder tmpCookies = new StringBuilder();
for (Cookie c : cookies) {
tmpCookies.append(c.toString()).append(";");
log.info(String.valueOf(tmpCookies));
}
COOKIE = tmpCookies.toString();
log.debug("xxlJob 登录成功");
}else {
log.debug("xxlJob 登录失败:{}",postMethod.getStatusCode());
}
}
/**
* 创建任务
* @param params
* @return
* @throws IOException
*/
public JSONObject createJob(JSONObject params) throws IOException {
return doPost(clientConfigProperties.getJobInfoAddUrl(), params);
}
/**
* 更新任务
* @param params
* @return
* @throws IOException
*/
public JSONObject updateJob(JSONObject params) throws IOException {
return doPost(clientConfigProperties.getJobInfoUpdateUrl(), params);
}
/**
* 根据任务 ID 加载
* @param id
* @return
* @throws IOException
*/
public JSONObject loadById(int id) throws IOException {
log.info("loadById: {}",id);
return doGet(String.format(clientConfigProperties.getJobInfoLoadByIdUrl(),id));
}
/**
* 删除任务
* @param id 任务 ID
* @return
* @throws IOException
*/
public JSONObject deleteJob(int id) throws IOException {
log.info("deleteJob: {}",id);
return doGet(String.format(clientConfigProperties.getJobInfoDeleteUrl(),id));
}
/**
* 开启任务
* @param id 任务 ID
* @return
* @throws IOException
*/
public JSONObject startJob(int id) throws IOException {
log.info("startJob: {}",id);
return doGet(String.format(clientConfigProperties.getJobInfoStartJobUrl(),id));
}
/**
* 停止任务
* @param id 任务 ID
* @return
* @throws IOException
*/
public JSONObject stopJob(int id) throws IOException {
log.info("stopJob: {}",id);
return doGet(String.format(clientConfigProperties.getJobInfoStopJobUrl(),id));
}
/**
* 创建执行器
* @param params
* @return
* @throws IOException
*/
public JSONObject createJobGroup(JSONObject params) throws IOException {
return doPost(clientConfigProperties.getJobGroupSaveUrl(), params);
}
/**
* 执行器列表
* @param params
* @return
* @throws IOException
*/
public JSONObject jobGroupPageList(JSONObject params) throws IOException {
params.put("start",Optional.ofNullable(params.getInteger("start")).orElse(0));
params.put("length", Optional.ofNullable(params.getInteger("length")).orElse(10));
return doPost(clientConfigProperties.getJobGroupPageListUrl(),params);
}
/**
* 任务列表
* @param params
* @return
* @throws IOException
*/
public JSONObject jobInfoPageList(JSONObject params) throws IOException {
params.put("start",Optional.ofNullable(params.getInteger("start")).orElse(0));
params.put("length", Optional.ofNullable(params.getInteger("length")).orElse(10));
return doPost(clientConfigProperties.getJobInfoPageListUrl(),params);
}
/**
* 发起 GET 请求
* @param url
* @return
* @throws IOException
*/
private JSONObject doGet(String url) throws IOException {
GetMethod get = new GetMethod(url);
get.setRequestHeader("cookie", COOKIE);
httpClient.executeMethod(get);
return readResponse(get);
}
/**
* post 请求
* @param url
* @param params
* @return
* @throws IOException
*/
private JSONObject doPost(String url,JSONObject params) throws IOException {
PostMethod post = new PostMethod(url);
post.setRequestHeader("cookie", COOKIE);
List<NameValuePair> pairList = new ArrayList<>();
params.forEach((k,v)-> pairList.add(new NameValuePair(k, v.toString())));
NameValuePair[] arr = pairList.toArray(new NameValuePair[0]);
post.setRequestBody(arr);
post.setRequestHeader(POST_FORM_CONTENT_TYPE_HEADER);
httpClient.executeMethod(post);
return readResponse(post);
}
/**
* 处理响应内容
* @param httpMethod
* @return
* @throws IOException
*/
private JSONObject readResponse(HttpMethod httpMethod) {
if (httpMethod.getStatusCode() == HttpStatus.SC_OK) {
try (InputStream inputStream = httpMethod.getResponseBodyAsStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
return JSON.parseObject(bufferedReader.lines().collect(Collectors.joining(System.lineSeparator())));
} catch (IOException e) {
log.error("读取响应失败:{}", e.getMessage(), e);
JSONObject error = new JSONObject();
error.put("code",HttpStatus.SC_INTERNAL_SERVER_ERROR);
error.put("msg","响应内容读取失败:"+e.getMessage());
return error;
}
}
return new JSONObject();
}
}
因为管理中心需要登陆认证,每次请求都需要携带登陆成功后的cookie,所以在login上加上@PostConstruct,当依赖注入结束后,自动执行登陆操作并将cookie初始化
测试类:
package com.xxl.job.executor.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xxl.job.executor.model.XxlJobInfo;
import com.xxl.job.executor.model.XxlJobInfoBO;
import com.xxl.job.executor.service.TaskService;
import com.xxl.job.executor.util.XxlJobClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
/**
* @author: Larry
* @Date: 2024 /02 /24 / 0:36
* @Description:
*/
@Service
public class TaskServiceImpl implements TaskService {
@Resource
XxlJobClient xxlJobClient;
private static final String DefaultGlueType = "BEAN";
private static final String DefaultExecutorRouteStrategy = "FIRST";
private static final String DefaultExecutorBlockStrategy = "SERIAL_EXECUTION";
private static final String DefaultScheduleType = "CRON";
private static final String DefaultMisfireStrategy = "DO_NOTHING";
public void addTask(XxlJobInfo xxlJobInfo) throws IOException {
xxlJobInfo.setExecutorBlockStrategy(DefaultExecutorBlockStrategy);
xxlJobInfo.setGlueType(DefaultGlueType);
xxlJobInfo.setExecutorRouteStrategy(DefaultExecutorRouteStrategy);
xxlJobInfo.setScheduleType(DefaultScheduleType);
xxlJobInfo.setAuthor("you_author");
xxlJobInfo.setMisfireStrategy(DefaultMisfireStrategy);
System.out.println(xxlJobInfo);
String jsonString = JSON.toJSONString(xxlJobInfo);
JSONObject jsonObject = JSONObject.parseObject(jsonString);
xxlJobClient.createJob(jsonObject);
}
}
由于大部分配置都很固定或没有太大意义,所以暂时将其写死,也可以加个配置了,或者常用属性添加枚举类进行配置;
测试:
可以看到操作成功,我们成功实现了远程调用
总结
xxl-job是一个简单易上手的分布式任务调度框架,虽然本身不支持远程调用,但可以使用httpclient或者restTemple等远程连接工具进行连接。要进行远程连接的原因:我个人认为最主要的一个原因是添加定时任务的人不一定是代码工程师,可能是公司管理员,学校领导等,他们不懂代码,所以要将其封装成一个相对好操作的页面。定时任务应用场景很多,例如:定期删除日志,数据异步更新,数据备份,12306提前生成票信息,定时考试等,由于篇幅关系暂时不展开论述了,如果大家对具体的业务设计实现感兴趣,点赞,收藏,评论,我们下期继续,数据好看的话,我会将一些典型的定时任务,延迟任务案例的设计和实现展开论述,同时将一些封装好的工具类也给到大家,谢谢支持!另外大家使用技术栈要结合使用场景,不要为了用而用,如果你的项目只是单体,那么springTask已经能满足你绝大部分需要了