研发说:API 请求量到底啥情况呀?统计发粗来(万一访问量一直激增,导致服务宕了,要扣我绩效滴)。
运维说:定期统计一下服务器内存、CPU占用率(万一出故障了,这个锅谁来背?)
业务说:记得把订单支付状态通知一下业务线(我很谨慎,不然都不知道钱支付出去了,妥妥避坑)。
产品说:把每天凌晨 2 点通知用户还款功能简单实现一下(功能很简单,上午实现,下午上线,怎么实现我不管)。
运营说:把每月的业务情况统计粗来(我要向上管理,向上汇报要用到)。
财务说:把账户日末余额统计统计,发个报表粗来(我要去谈费率,为公司节省成本,不然年底就没奖杯可拿啦)。
老板说:每月 15 号发工资,记得把发薪结果统计粗来(我看看到底还能再创(砍)多少辉(人)煌(头))。
很显然,如上需求大概率都需要定时任务来支撑。在日常项目研发中,定时任务可谓是必不可少的一环。本次主要借助 Spring Boot 来谈谈如何实现定时任务。
1. 静态定时任务
所谓静态定时任务是指应用跑起来后,任务的执行时间无法进行动态修改。实现起来也比较简单,只需通过 Spring Boot 内置注解 @Scheduled 来实现,默认是启动单线程来跑任务,可以通过配置线程池开启多线程,下面逐一学习一下。
1.1. 单线程定时任务
1.1.1 开启定时任务功能
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
1.1.2 创建任务类
@Component
public class DownLoadTask {
private static final Log logger = LogFactory.getLog(DownLoadTask.class);
@Scheduled(cron = "0 0/5 * * * ?")
public void justDoIt() {
logger.info("开始下载银行对账文件");
logger.info("银行对账文件下载完成,进行解密操作");
logger.info("银行对账文件下载解密完成");
}
}
@Scheduled:主要用来完成任务的配置,如执行时间、间隔时间、延迟时间等等,其中有如下配置格式,可以自行体验体验。
1.1.3 运行验证
实现了一个每 5 分钟去银行下载一个对账文件的任务,跑起来效果如下。
回头去看,SpringBoot 开启定时任务的确很简单,几行代码就轻松搞定,so easy~。
但是,疑问来了。
疑问:若同时开启两个任务,会存在什么效果呢?若分别下载 A、B 两家银行的对账文件,如何支持呢?
@Component
public class DownLoadTask {
private static final Log logger = LogFactory.getLog(DownLoadTask.class);
@Scheduled(cron = "0/1 * * * * ?")
public void justDoItA() {
logger.info("开始下载银行 A 的对账文件");
logger.info("银行 A 对账文件下载完成,进行解密操作");
logger.info("银行 A 对账文件下载解密完成");
}
@Scheduled(cron = "0/1 * * * * ?")
public void justDoItB() {
logger.info("开始下载银行 B 的对账文件");
logger.info("银行 B 对账文件下载完成,进行解密操作");
logger.info("银行 B 对账文件下载解密完成");
}
}
程序跑起来,效果如下。
很显然,一个线程先办完 A,然后办 B,等上一个事儿办完了才办下一个事儿,不支持多线程。若项目里有多个任务要并行执行,而 Spring Boot 默认单线程来执行任务的方案就差点意思了。
不过无妨,Spring Boot 有开启多线程的方案,接下来看看如何开启多线程来执行任务。
1.2. 多线程定时任务
1.2.1 自定义线程池
@Configuration
public class SchedulerConfig {
@Bean(name = "bankThreadPool")
public Executor bankExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数为 3
executor.setCorePoolSize(3);
// 最大线程数为10
executor.setMaxPoolSize(10);
// 任务队列的大小
executor.setQueueCapacity(3);
// 线程前缀名
executor.setThreadNamePrefix("bankExecutor-");
// 线程存活时间
executor.setKeepAliveSeconds(30);
// 初始化
executor.initialize();
return executor;
}
}
1.2.2 开启异步执行
@Component
@EnableAsync
public class DownLoadTask {
private static final Log logger = LogFactory.getLog(DownLoadTask.class);
@Async("bankThreadPool")
@Scheduled(cron = "0/1 * * * * ?")
public void justDoIt() {
logger.info("开始下载银行 A 的对账文件");
logger.info("银行 A 对账文件下载完成,进行解密操作");
logger.info("银行 A 对账文件下载解密完成");
}
@Async("bankThreadPool")
@Scheduled(cron = "0/1 * * * * ?")
public void justDoIt2() {
logger.info("开始下载银行 B 的对账文件");
logger.info("银行 B 对账文件下载完成,进行解密操作");
logger.info("银行 B 对账文件下载解密完成");
}
}
1.2.3 运行验证
显而易见,线程池已生效,多线程执行任务,任务之间相对独立、互不影响。
此时,简单的几行配置代码,足矣满足下载银行对账文件等简易场景的定时任务。
但是,任务执行的时间放在代码里总有种不妥,若因为走了狗屎运想调整一下任务执行的时间,那岂不是要重新改代码,重新发布上线?
疑问来了:如何动态修改任务执行的时间,而无需重新发布重启服务呢?
莫急,继续往下瞅。
2. 动态定时任务
由于 Spring Boot 内置的 @Scheduled 注解无法动态修改任务执行的时间,而实现 SchedulingConfigurer 接口提供了动态修改任务执行时间的可能性。
另外要维护任务执行的时间配置方式有很多种,思想很重要,实现无所谓,则其一便可。
2.1. 定义任务类
/**
* 动态定时任务实现步骤
* 步骤1:定义定时任务 DownLoadTaskV3 类实现 SchedulingConfigurer 接口;
* 步骤2:编写定时任务要执行的业务逻辑;
* 步骤3:数据库中配置任务执行的具体时间规则,记住任务名称
* 步骤4:根据任务名称从数据库获取 Cron 参数,设置任务触发器,触发任务执行。
* (仅抛砖引玉,可作进一步的抽象)
*/
@Component
@EnableScheduling
public class DownLoadTaskV3 implements SchedulingConfigurer {
private static final Log logger = LogFactory.getLog(DownLoadTaskV3.class);
@Autowired
private TaskInfoRepository taskInfoRepository;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
Runnable task = new Runnable() {
@Override
public void run() {
// 步骤2:编写定时任务要执行的业务逻辑(可以进一步抽象)。
logger.info("V3-开始下载银行 C 的对账文件");
logger.info("V3-银行 C 对账文件下载完成,进行解密操作");
logger.info("V3-银行 C 对账文件下载解密完成");
}
};
// 步骤 4:根据任务名称从数据库获取 Cron 参数,设置任务触发器,触发任务执行。
Trigger trigger = new Trigger() {
/**
* 每一次任务触发,都会调用一次该方法
* 然后重新获取下一次的执行时间
*/
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
// 方式一:执行时间硬编码
//String cron = "0/1 * * * * ?";
// 方式二:动态获取执行时间(从数据库、redis 等都可以做任务执行时间的存储管理,本次以数据库为例)
TaskInfo taskInfo = new TaskInfo();
// 数据库配置的任务名称,通过任务名称获取对应的任务执行时间
taskInfo.setJobName("downLoadTaskV3");
Optional<TaskInfo> taskInfoOptional = taskInfoRepository.findOne(Example.of(taskInfo));
// 获取配置的任务执行时间 cron 表达式
String cron = taskInfoOptional.get().getCron();
CronTrigger trigger = new CronTrigger(cron);
return trigger.nextExecutionTime(triggerContext);
}
};
// 设置任务触发器,触发任务执行。
taskRegistrar.addTriggerTask(task, trigger);
}
}
2.2. 创建任务信息表
CREATE TABLE `SC_TASK_INFO` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`cron` varchar(32) DEFAULT NULL COMMENT '定时执行',
`job_name` varchar(256) DEFAULT NULL COMMENT '任务名称',
`status` char(1) DEFAULT '0' COMMENT '任务开启状态 0-关闭 2-开启',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) COMMENT='定时任务表';
INSERT INTO `SC_TASK_INFO` VALUES ('1', '0/10 * * * * ?', 'downLoadTaskV3', '2', '2020-03-01 16:43:50', '2020-06-11 11:06:09');
本次只用到了表中的 cron(定时表达式)、job_name(任务名称)两个字段,其它字段后续集成 Quartz 才会用到,可先忽略。
2.3. 创建实体类
@Entity
@Table(name = "sc_task_info")
public class TaskInfo implements Serializable {
@Id
private Integer id;
@Column
private String cron;
@Column
private String jobName;
@Column
private String status;
@Column
private Date createTime;
@Column
private Date updateTime;
// 提供 setter/getter 方法
}
2.4. 定义持久化接口
public interface TaskInfoRepository extends JpaRepository<TaskInfo, Integer> {
}
2.5. 引入依赖以及相关配置
主要是完成从数据库查询指定任务名称对应的定时配置,实现方式会有很多种,不要局限于本文提及的 JPA,可参考历史分享《玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)》引入 JPA、数据库连接依赖以及 application.properties 完成数据库连接配置。
2.6. 运行验证
库中对于 downLoadTaskV3 任务默认配置的时间为每 10 秒执行一次。
控制台输出如下。
手动修改数据库,把任务执行的时间表达式修改为每 1 秒执行一次。
控制台输出效果如下,很显然已经生效了。
至此,定时任务的时间就可以动态修改生效了,若再实现一个页面进行修改任务执行时间的值,其实也挺爽。
这种方案其实可以称为是简易版的 Quartz,在一定程度上也能解决一定的业务场景问题,但是若做更复杂的动作,例如启停任务、删除任务等等操作,实现起来则稍显复杂,此时便可以通过集成 Quartz 等开源任务框架来实现,而鉴于集成 Quartz 框架的动态管理任务代码较多咱们下一篇再分享。
3. 例行回顾
本文是 Spring Boot 项目集成定时任务首篇讲解,主要分享了如下部分:
玩转 Spring Boot 集成定时任务首篇就写到这里,下次一起集成 Quratz 框架并实现任务动态管理。
历史系列文章:
玩转 Spring Boot 入门篇 玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP) 玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持) 玩转 Spring Boot 集成篇(Redis) 玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin) 玩转 Spring Boot 集成篇(RabbitMQ)