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

本文介绍了如何在 Spring Boot 中使用 @Scheduled 实现静态定时任务,包括单线程和多线程执行,并探讨了动态修改任务执行时间的方法,通过数据库存储定时表达式实现实时更新。

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

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

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

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

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

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

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

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

d95f3ff01806118ad38060ce9f0134f4.png

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);
    }
}
  • @EnableScheduling:用来开启定时任务功能,可以检测 Spring 管理的 bean 上 @Scheduled 的注解,系统默认会自动启动一个线程,来调度执行定时任务。

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:主要用来完成任务的配置,如执行时间、间隔时间、延迟时间等等,其中有如下配置格式,可以自行体验体验。

f63ecd0ad4b208c9e10a2585deaf8925.png

1.1.3 运行验证

实现了一个每 5 分钟去银行下载一个对账文件的任务,跑起来效果如下。

553840c9d4978cbf402fcec35f12e48e.png

回头去看,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 对账文件下载解密完成");
    }
}

程序跑起来,效果如下。

e38d8e18e012dfe1c36533487854cf1f.png

很显然,一个线程先办完 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;
    }
}
  • @Bean(name = "bankThreadPool"):方法级别上的注解,用来定义实例化线程池,别名为 bankThreadPool。

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 对账文件下载解密完成");
    }
}
  • @EnableAsync:表示开启对异步任务的支持,就可以使用多线程了。

  • @Async:在方法上加入这个注解,异步执行方法。Spring 会从指定的线程池中获取新线程来执行方法,@Async("name") 会用指定 name 的线程池来处理。

1.2.3 运行验证

6f0fc5f673b11bb2808d99c05081fdc3.png

显而易见,线程池已生效,多线程执行任务,任务之间相对独立、互不影响。

此时,简单的几行配置代码,足矣满足下载银行对账文件等简易场景的定时任务。

但是,任务执行的时间放在代码里总有种不妥,若因为走了狗屎运想调整一下任务执行的时间,那岂不是要重新改代码,重新发布上线?

疑问来了:如何动态修改任务执行的时间,而无需重新发布重启服务呢?

莫急,继续往下瞅。

2. 动态定时任务

由于 Spring Boot 内置的 @Scheduled 注解无法动态修改任务执行的时间,而实现 SchedulingConfigurer 接口提供了动态修改任务执行时间的可能性。

另外要维护任务执行的时间配置方式有很多种,思想很重要,实现无所谓,则其一便可。

  • 可以放在配置文件里,然后判断文件的修改时间是否发生变化,若变化了则重新读取配置的时间值;

  • 可以放在 Redis 里,然后任务执行的时候获取 Redis 里缓存的定时任务时间值;

  • 可以放在数据库里,然后任务执行的时候根据任务名称获取库中维护的定时任务时间值。(本次采取这个方案)

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);
    }
}
  • ScheduledTaskRegistrar.addTriggerTask(Runnable task, Trigger trigger):参数 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 秒执行一次。

fde5f4fbbe57aafbcb2fcd7117cd6edf.png

控制台输出如下。

d9692fe7f965b7325b69005475c30b7b.png

手动修改数据库,把任务执行的时间表达式修改为每 1 秒执行一次。

c604da453978f37a39af5424ffebd0cf.png

控制台输出效果如下,很显然已经生效了。

93b8a7ea2e083832c5ffe4a3d8a03fef.png

至此,定时任务的时间就可以动态修改生效了,若再实现一个页面进行修改任务执行时间的值,其实也挺爽。

这种方案其实可以称为是简易版的 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)

dad7f4ecb3fbeb15bd81472157a4fd66.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值