研发说:API 请求量到底啥情况呀?统计发粗来(万一访问量一直激增,导致服务宕了,要扣我绩效滴)。
运维说:定期统计一下服务器内存、CPU占用率(万一出故障了,这个锅谁来背?)
业务说:记得把订单支付状态通知一下业务线(我很谨慎,不然都不知道钱支付出去了,妥妥避坑)。
产品说:把每天凌晨 2 点通知用户还款功能简单实现一下(功能很简单,上午实现,下午上线,怎么实现我不管)。
运营说:把每月的业务情况统计粗来(我要向上管理,向上汇报要用到)。
财务说:把账户日末余额统计统计,发个报表粗来(我要去谈费率,为公司节省成本,不然年底就没奖杯可拿啦)。
老板说:每月 15 号发工资,记得把发薪结果统计粗来(我看看到底还能再创(砍)多少辉(人)煌(头))。
很显然,如上需求大概率都需要定时任务来支撑。在日常项目研发中,定时任务可谓是必不可少的一环。本次主要借助 Spring Boot 来谈谈如何实现定时任务。
1. 静态定时任务
所谓静态定时任务是指应用跑起来后,任务的执行时间无法进行动态修改。实现起来也比较简单,只需通过 Spring Boot 内置注解 @Scheduled 来实现,默认是启动单线程来跑任务,可以通过配置线程池开启多线程,下面逐一学习一下。
1.1. 单线程定时任务
1.1.1 开启定时任务功能
代码语言:javascript复制@SpringBootApplication@EnableSchedulingpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}
- @EnableScheduling:用来开启定时任务功能,可以检测 Spring 管理的 bean 上 @Scheduled 的注解,系统默认会自动启动一个线程,来调度执行定时任务。
1.1.2 创建任务类
代码语言:javascript复制@Componentpublic 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 两家银行的对账文件,如何支持呢?
代码语言:javascript复制@Componentpublic 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 自定义线程池
代码语言:javascript复制@Configurationpublic 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; }}
- @Bean(name = "bankThreadPool"):方法级别上的注解,用来定义实例化线程池,别名为 bankThreadPool。
1.2.2 开启异步执行
代码语言:javascript复制@Component@EnableAsyncpublic 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 对账文件下载解密完成"); }}
- @EnableAsync:表示开启对异步任务的支持,就可以使用多线程了。
- @Async:在方法上加入这个注解,异步执行方法。Spring 会从指定的线程池中获取新线程来执行方法,@Async("name") 会用指定 name 的线程池来处理。
1.2.3 运行验证
显而易见,线程池已生效,多线程执行任务,任务之间相对独立、互不影响。
此时,简单的几行配置代码,足矣满足下载银行对账文件等简易场景的定时任务。
但是,任务执行的时间放在代码里总有种不妥,若因为走了狗屎运想调整一下任务执行的时间,那岂不是要重新改代码,重新发布上线?
疑问来了:如何动态修改任务执行的时间,而无需重新发布重启服务呢?
莫急,继续往下瞅。
2. 动态定时任务
由于 Spring Boot 内置的 @Scheduled 注解无法动态修改任务执行的时间,而实现 SchedulingConfigurer 接口提供了动态修改任务执行时间的可能性。
另外要维护任务执行的时间配置方式有很多种,思想很重要,实现无所谓,则其一便可。
- 可以放在配置文件里,然后判断文件的修改时间是否发生变化,若变化了则重新读取配置的时间值;
- 可以放在 Redis 里,然后任务执行的时候获取 Redis 里缓存的定时任务时间值;
- 可以放在数据库里,然后任务执行的时候根据任务名称获取库中维护的定时任务时间值。(本次采取这个方案)
2.1. 定义任务类
代码语言:javascript复制/** * 动态定时任务实现步骤 * 步骤1:定义定时任务 DownLoadTaskV3 类实现 SchedulingConfigurer 接口; * 步骤2:编写定时任务要执行的业务逻辑; * 步骤3:数据库中配置任务执行的具体时间规则,记住任务名称 * 步骤4:根据任务名称从数据库获取 Cron 参数,设置任务触发器,触发任务执行。 * (仅抛砖引玉,可作进一步的抽象) */@Component@EnableSchedulingpublic 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); }}
- ScheduledTaskRegistrar.addTriggerTask(Runnable task, Trigger trigger):参数 task 中定义执行业务逻辑,在 trigger中进行修改定时任务的执行时间。
2.2. 创建任务信息表
代码语言:javascript复制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. 创建实体类
代码语言:javascript复制@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. 定义持久化接口
代码语言:javascript复制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 内置注解实现静态定时任务;
- 提了一嘴四种任务时间配置格式;
- 分享了如何开启多线程跑任务?
- 尝试实现了动态定时任务。
玩转 Spring Boot 集成定时任务首篇就写到这里,下次一起集成 Quratz 框架并实现任务动态管理。