序言
使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:
- 一、基于注解(@Scheduled)
- 二、基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。
- 三、基于注解设定多线程定时任务
静态:基于注解
基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
创建定时器
使用SpringBoot基于注解来创建定时任务非常简单,只需几行代码便可完成。
代码如下:
@Component @Configuration //1.主要用于标记配置类,兼备Component的效果。 @EnableScheduling // 2.开启定时任务 public class SaticScheduleTask { //3.添加定时任务 @Scheduled(cron = "0/5 * * * * ?") //或直接指定时间间隔,例如:5秒 //@Scheduled(fixedRate=5000) private void configureTasks() { System.err.println("执行静态定时任务时间: " + LocalDateTime.now()); } }
Cron表达式参数分别表示:
- */5 * * * * ? :每隔5秒执行一次
- 0 */1 * * * ? :每隔1分钟执行一次
- 0 0 23 * * ? :每天23点执行一次
- 0 0 1 * * ? :每天凌晨1点执行一次:
- 0 0 1 1 * ? :每月1号凌晨1点执行一次
- 0 0 23 L * ? : 每月最后一天23点执行一次
- 0 0 1 ? * L :每周星期天凌晨1点实行一次
- 0 26,29,33 * * * ? : 在26分、29分、33分执行一次
- 0 0 0,13,18,21 * * ? : 每天的0点、13点、18点、21点都执行一次
Cron表达式由7个部分组成,各部分用空格隔开,Cron表达式的7个部分从左到右代表的含义如下:
秒 分 时 日 月 周 年
其中 年是可选的。
字段名 | 允许的值 | 允许的特殊字符 |
秒 | 0-59 | ,- * / |
分 | 0-59 | ,- * / |
时 | 0-23 | ,- * / |
日 | 1-31 | ,- * ? / L W C |
月 | 1-12 or JAN-DEC | ,- * / |
周 | 1-7 or SUN-SAT | ,- * ? / L C # |
年(可选字段) | empty,1970-2099 | ,- * / |
符号说明
- ,:表示列出枚举值值。例如在分使用5,20,则意味着在5和20分每分钟触发一次。
- -:表示范围。例如在分使用5-20,表示从5分到20分钟每分钟触发一次。
- * :表示匹配该域的任意值。假如在分域使用*,即表示每分钟都会触发事件。
- / :表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次。
- ? :只能用在周和日。它也匹配域的任意值,但实际不会。因为周和日会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?,其中最后一位只能用?,而不能使用,如果使用表示不管星期几都会触发,实际上并不是这样。
- L : 表示最后,只能出现在日和周,如果在日使用5L,意味着在最后的一个星期四触发。
- W:表示有效工作日(周一到周五),只能出现在周域,系统将在离指定日期的最近的有效工作日触发事件。例如:在日使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。
- #:用于确定每个月第几个星期几,只能出现在周。例如在4#2,表示某月的第二个星期三。
@Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
动态:基于接口
基于接口(SchedulingConfigurer)
导入依赖包pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.0.4.RELEASE</version> </parent> <dependencies> <dependency><!--添加Web依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><!--添加MySql依赖 --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西--> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency><!-- 添加mybatis依赖 --> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> <scope>compile</scope> </dependency> </dependencies>
添加数据库记录:
DROP DATABASE IF EXISTS `socks`; CREATE DATABASE `socks`; USE `SOCKS`; DROP TABLE IF EXISTS `cron`; CREATE TABLE `cron` ( `cron_id` varchar(30) NOT NULL PRIMARY KEY, `cron` varchar(30) NOT NULL ); INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');
然后在项目中的application.properties 文件中
添加数据源:
server.port = 8888 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url= jdbc:mysql://127.0.0.1:3306/ordersystem?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root
创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。
@Component @Configuration //1.主要用于标记配置类,兼备Component的效果。 @EnableScheduling // 2.开启定时任务 public class DynamicScheduleTask implements SchedulingConfigurer { @Mapper public interface CronMapper { @Select("select cron from cron limit 1") public String getCron(); } @Autowired //注入mapper @SuppressWarnings("all") CronMapper cronMapper; /** * 执行定时任务. */ @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask( //1.添加任务内容(Runnable) () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()), //2.设置执行周期(Trigger) triggerContext -> { //2.1 从数据库获取执行周期 String cron = cronMapper.getCron(); //2.2 合法性校验. if (StringUtils.isEmpty(cron)) { // Omitted Code .. } //2.3 返回执行周期(Date) return new CronTrigger(cron).nextExecutionTime(triggerContext); } ); } }
注意:如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。
多线程
基于注解设定多线程定时任务
创建多线程定时任务
例子:我下订单后,要求订单在30分钟内如果未支付则自动取消该订单
package com.shijiu.component; import cn.hutool.core.date.DateUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.shijiu.dao.mapper.ShopOrderMapper; import com.shijiu.pojo.ShopOrder; import com.shijiu.service.ShopOrderService; import com.shijiu.service.impl.ShopOrderServiceImpl; import io.swagger.annotations.ApiOperation; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; /** * @Author * @Date Created in 2023/4/27 9:52 * @DESCRIPTION: * @Version V1.0 */ //@Component注解用于对那些比较中立的类进行注释; //相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释 @Component // 1.开启定时任务 @EnableScheduling /** 2.开启多线程(定时任务检查删除订单) */ @EnableAsync @SuppressWarnings("all") public class MultithreadScheduleTask { @Resource private ShopOrderMapper shopOrderMapper; @Async @Scheduled(fixedDelay = 60000) @ApiOperation("/* 间隔1分钟定时检查数据库 */") public void first() throws InterruptedException { LambdaQueryWrapper<ShopOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ShopOrder::getStatus,"0").eq(ShopOrder::getIsDeleted,"0"); //拿到所有的订单 List<ShopOrder> shopOrderList = shopOrderMapper.selectList(wrapper); List<ShopOrder> filteredOrders = shopOrderList.stream() .filter(order -> Math.ceil(DateUtil.betweenMs(order.getCreateTime(), order.getOverTime())) / 60000.0 > 30) // 保留订单结束时间与创建时间间隔超过30分钟的订单 .collect(Collectors.toList()); // 将满足条件的订单收集到一个新的集合中 //double result = Math.ceil(DateUtil.betweenMs("2023-04-27 10:05:03", "2023-04-27 10:07:02") / 60000.0); // 对筛选出来的订单执行xxx操作 System.out.println(filteredOrders); filteredOrders.forEach(order -> { // 对订单执行xxx操作,例如: String orderId = order.getId(); ShopOrder shopOrder = new ShopOrder(); shopOrder.setId(shopOrderId); shopOrder.setIsDeleted("1"); shopOrderMapper.updateById(shopOrder); }); } }
打印结果: