前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

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

作者头像
一猿小讲
发布2022-02-25 09:50:15
1.1K1
发布2022-02-25 09:50:15
举报
文章被收录于专栏:一猿小讲一猿小讲

研发说:API 请求量到底啥情况呀?统计发粗来(万一访问量一直激增,导致服务宕了,要扣我绩效滴)。

运维说:定期统计一下服务器内存、CPU占用率(万一出故障了,这个锅谁来背?)

业务说:记得把订单支付状态通知一下业务线(我很谨慎,不然都不知道钱支付出去了,妥妥避坑)。

产品说:把每天凌晨 2 点通知用户还款功能简单实现一下(功能很简单,上午实现,下午上线,怎么实现我不管)。

运营说:把每月的业务情况统计粗来(我要向上管理,向上汇报要用到)。

财务说:把账户日末余额统计统计,发个报表粗来(我要去谈费率,为公司节省成本,不然年底就没奖杯可拿啦)。

老板说:每月 15 号发工资,记得把发薪结果统计粗来(我看看到底还能再创(砍)多少辉(人)煌(头))。

很显然,如上需求大概率都需要定时任务来支撑。在日常项目研发中,定时任务可谓是必不可少的一环。本次主要借助 Spring Boot 来谈谈如何实现定时任务。

1. 静态定时任务

所谓静态定时任务是指应用跑起来后,任务的执行时间无法进行动态修改。实现起来也比较简单,只需通过 Spring Boot 内置注解 @Scheduled 来实现,默认是启动单线程来跑任务,可以通过配置线程池开启多线程,下面逐一学习一下。

1.1. 单线程定时任务

1.1.1 开启定时任务功能

代码语言:javascript
复制
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
  • @EnableScheduling:用来开启定时任务功能,可以检测 Spring 管理的 bean 上 @Scheduled 的注解,系统默认会自动启动一个线程,来调度执行定时任务。

1.1.2 创建任务类

代码语言:javascript
复制
@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 两家银行的对账文件,如何支持呢?

代码语言:javascript
复制
@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 自定义线程池

代码语言:javascript
复制
@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;
    }
}
  • @Bean(name = "bankThreadPool"):方法级别上的注解,用来定义实例化线程池,别名为 bankThreadPool。

1.2.2 开启异步执行

代码语言:javascript
复制
@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 对账文件下载解密完成");
    }
}
  • @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
@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);
    }
}
  • 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 框架并实现任务动态管理。

历史系列文章:

玩转 Spring Boot 入门篇 玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP) 玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持) 玩转 Spring Boot 集成篇(Redis) 玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin) 玩转 Spring Boot 集成篇(RabbitMQ)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-02-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档