玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)(七)

2022-09-23 20:47:13 浏览数 (1)

研发说: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 框架并实现任务动态管理。

0 人点赞