定时任务是业务场景中一种不可或缺的通用能力,运用适合的定时任务能够快速解决业务问题,同时又能避免过度设计带来的资源浪费。
一、Timer
Timer是JDK自带的java.util包下的一个类,作用就是定时器。下面是Timer类中的构造方法、指定任务等方法
构造方法 | |
Timer() | 创建一个定时器 |
Timer(boolean isDaemon) | 创建一个定时器,指定线程是否为守护线程 |
Timer(String name) | 创建一个定时器,指定线程的名词 |
Timer(String name, boolean isDaemon) | 创建一个定时器,指定线程的名词 + 是否为守护线程 |
其它方法 | |
void schedule(TimerTask task, long delay) | 安排在指定延迟后,执行指定的任务 |
void schedule(TimerTask task, Date time) | 安排在指定的时间执行指定的任务 |
void schedule(TimerTask task, long delay, long period) | 安排指定的任务,从指定的延迟后开始进行重复的固定延迟执行 |
void schedule(TimerTask task, Date firstTime, long period) | 安排指定的任务在指定的时间开始进行重复的固定延迟执行 |
void scheduleAtFixedRate(TimerTask task, long delay, long period) | 安排指定的任务在指定的延迟后开始进行重复的固定速率执行 |
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 安排指定的任务在指定的时间开始进行重复的固定速率执行 |
void cancel() | 终止计时器,丢弃所有当前已安排的任务 |
int purge() | 从计时器的任务队列中移除所有已取消的任务 |
1.1 实现功能
public class TimerTest {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
Date firstTime = sdf.parse("14:00:56"); // 开始执行时间
timer.schedule(new LogTimerTask(), firstTime, 1000 * 10); // 10秒执行一次
}
}
// 定时任务类
class LogTimerTask extends TimerTask {
// 在run()方法中编写具体需要定时执行的任务
@Override
public void run() {
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss SSS");
String strTime = sdf.format(new Date());
System.out.println(strTime + "执行了一次定时任务!!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.2 运行测试
二、Spring Task
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.10</version>
</dependency>
2.2 编写定时任务逻辑
import cn.hutool.core.date.DateUtil;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Component
public class TaskTest {
@Scheduled(cron = "0/3 * * * * ?") // 每三秒执行一次
public static void ScheduledTasksTest1() {
// 获取前一天的日期时间并格式化
String createTime = DateUtil.offsetDay(new Date(), -1).toString(new SimpleDateFormat("HH:mm:ss"));
log.info("ScheduledTasksTest1本次定时任务开始执行时间为:" + createTime);
}
@Scheduled(cron = "0/5 * * * * ?") // 每五秒执行一次
public static void ScheduledTasksTest2() {
// 获取前一天的日期时间并格式化
String createTime = DateUtil.offsetDay(new Date(), -1).toString(new SimpleDateFormat("HH:mm:ss"));
log.info("ScheduledTasksTest2为:" + createTime);
}
}
2.3 在启动类上添加@EnableScheduling注解
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.4 测试
三、XXL-JOB
XXL-JOB是一个轻量级分布式任务调度框架,它的核心设计理念是把任务调度分为两个核心部分:调度中心(xxl-admin)和执行器。把任务调度隔离成两个部分,是一种中心化的设计,由调度中心来统一管理和调度各个接入的业务模块(也叫执行器),接入的业务模块(执行器)只需要接收调度信号,然后去执行具体的业务逻辑,两者可以各自的进行扩容。
3.1 下载源码并部署
Gitee地址:https://gitee.com/xuxueli0323/xxl-job
下载完成后,将sql文件运行到自己的数据库中,这是xxl-job需要的数据库。
数据库配置完后,打开xxl-job-admin模块的配置文件进行配置。
如果只是想要了解xxl-job的使用,只要注意修改如下几处配置即可。
- 端口号不要与自己项目的端口号项目冲突
- 要记住server.servlet.context-path配置的值,后面会用到
- 将数据库信息修改成自己的
3.2 引入maven依赖
在自己的项目中引入xxl-job依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
3.3 配置xxl-job配置文件
在自己项目的配置文件中配置如下信息。
- 要注意xxl.job.admin.address配置项,这里的值与源码xxl-job-admin模块配置文件中server.port项和server.servlet.context-path项相对应
- 记住xxl.job.executor.appname配置项的值,后期会用到
server:
port: 8081
xxl:
job:
accessToken: default_token
admin:
# 调度中心部署跟地址:如调度中心集群部署存在多个地址则用逗号分割,执行器将会使用该地址进行“执行器心跳注册”和“任务结果回调”
addresses: http://localhost:8080/xxl-job-admin
executor:
# AppName执行器心跳注册分组依据,为空则关闭自动注册
appname: xxl-job-demo
# 默认使用address注册,为空则使用ip:port
address:
# 执行器默认端口为9999;执行器默认ip为空标识自动获取ip,多网卡可手动设置指定ip,手动设置ip时将会绑定host
ip:
port: 9999
logpath:
# 执行器Log文件定期清理功能,指定日志保存天数,日志文件过期自动删除,限制至少保持3天,否则功能不生效
logretentiondays: 7
3.4 任务执行器配置类XxlJobConfig
在下载的源码工程中,复制xxl-job-executor-sample-springboot模块的XxlJobConfig类到自己想要定时执行任务的模块中。
或者直接复制如下代码
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@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.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
3.5 业务代码
public interface JobService {
/**
* 发送消息
*
* @param msg
* @return
*/
List<User> sendMessage(String msg);
}
在想要执行定时的方法上添加@XxlJob注解,并为其指定一个value值,代表自定义JobHandler,这个值后期会用到。
@Service
public class JobServiceImpl implements JobService {
public static Logger logger = LoggerFactory.getLogger(JobServiceImpl.class);
@XxlJob("sendMessage")
@Override
public List<User> sendMessage(String msg) {
logger.info("开始执行发送消息接口:{}", new SimpleDateFormat("HH:mm:ss").format(new Date()));
List<User> userList = queryUserList();
logger.info("{}\n", userList);
return userList;
}
/**
* 模拟Mapper层查询代码
*
* @return
*/
public static List<User> queryUserList() {
ArrayList<User> list = new ArrayList<>();
list.add(new User(001L, "小明", "男"));
list.add(new User(002L, "小天", "男"));
list.add(new User(003L, "小美", "女"));
return list;
}
}
3.6 配置xxl-job管理端任务
运行源码xxl-job-admin模块的启动类,在浏览器中访问http://localhost:8080/xxl-job-admin/jobgroup,进入xxl-job的可视化界面进行任务配置。
账号默认为:admin
密码默认为:123456
3.6.1 配置执行器管理
3.6.2 配置任务管理
3.6 测试
在运行了源码xxl-job-admin模块的XxlJobAdminApplication启动类的情况下,启动自己项目的启动类。
在xxl-job的任务调度中心任务管理模块中调试任务的操作。
点击执行一次,观察控制台输出,具体如下:
点击启动,观察控制台输出,具体如下:
模块结构如下:
四、基于Redis实现定时任务
基于Redis可以通过ZSet的方式或Redis的键空间通知实现定时任务
- 通过ZSet实现定时任务的思路是,将定时任务存放到ZSet集合中,并且将过期时间存储到ZSet的Score字段中,然后通过一个无线循环来判断当前时间内是否有需要执行的定时任务,如果有则进行执行。
- 通过Redis的键空间通知实现定时任务的思路是,给所有的定时任务设置一个过期时间,等到过期之后通过订阅过期消息就能感知到定时任务需要被执行了,此时执行定时任务即可。
- 默认情况下Redis是不开启键空间通知的,需要通过config set notify-keyspace-events Ex的命令手动开启。
五、基于RabbbitMQ实现定时任务
如果需要在某个事件发生之后或者之前的指定时间点完成某一项任务,可以通过RabbitMQ中间件来实现。
RabbitMQ实现这种问题有两种方式:
- 通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能
- 使用rabbitmq-delayed-message-exchange插件实现延迟功能