SpringBoot 动态多线程并发定时任务
一、简介
实现定时任务有多种方式:
- Timer:jdk 中自带的一个定时调度类,可以简单的实现按某一频度进行任务执行。提供的功能比较单一,无法实现复杂的调度任务。
- ScheduledExecutorService:也是 jdk 自带的一个基于线程池设计的定时任务类。其每个调度任务都会分配到线程池中的一个线程执行,所以其任务是并发执行的,互不影响。
- Spring Task:Spring 提供的一个任务调度工具,支持注解和配置文件形式,支持 Cron 表达式,使用简单但功能强大。
- Quartz:一款功能强大的任务调度器,可以实现较为复杂的调度功能,如每月一号执行、每天凌晨执行、每周五执行等等,还支持分布式调度,就是配置稍显复杂。
使用 spring 自带的,继承 SchedulingConfigurer 的方式。
源码地址:
Gitee: https://gitee.com/typ1805/tansci
GitHub: https://github.com/typ1805/tansci
二、编码实现
启动类添加 @EnableScheduling 注解
@EnableScheduling
@SpringBootApplication
public class TansciApplication {
public static void main(String[] args) {
SpringApplication.run(TansciApplication.class, args);
}
}
定时任务类
添加注解 @Component 注册到 spring 容器中。
/**
* @ClassName: ScheduledTask.java
* @ClassPath: com.tansci.common.task.ScheduledTask.java
* @Description: 定时任务
* @Author: tanyp
* @Date: 2022/2/25 9:30
**/
@Slf4j
@Component
public class ScheduledTask implements SchedulingConfigurer {
private volatile ScheduledTaskRegistrar registrar;
private final ConcurrentHashMap<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, CronTask> cronTasks = new ConcurrentHashMap<>();
@Autowired
private TaskContextService taskContextService;
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
registrar.setScheduler(Executors.newScheduledThreadPool(Constants.DEFAULT_THREAD_POOL));
this.registrar = registrar;
}
@PreDestroy
public void destroy() {
this.registrar.destroy();
}
/**
* @MonthName: refreshTask
* @Description: 初始化任务
* 1、从数据库获取执行任务的集合【TaskConfig】
* 2、通过调用 【refresh】 方法刷新任务列表
* 3、每次数据库中的任务发生变化后重新执行【1、2】
* @Author: tanyp
* @Date: 2022/2/25 9:42
* @Param: [tasks]
* @return: void
**/
public void refreshTask(List<TaskConfig> tasks) {
// 删除已经取消任务
scheduledFutures.keySet().forEach(key -> {
if (Objects.isNull(tasks) || tasks.size() == 0) {
scheduledFutures.get(key).cancel(false);
scheduledFutures.remove(key);
cronTasks.remove(key);
return;
}
tasks.forEach(task -> {
if (!Objects.equals(key, task.getTaskId())) {
scheduledFutures.get(key).cancel(false);
scheduledFutures.remove(key);
cronTasks.remove(key);
return;
}
});
});
// 添加新任务、更改执行规则任务
tasks.forEach(item -> {
String expression = item.getExpression();
// 任务表达式为空则跳过
if (StringUtils.isEmpty(expression)) {
return;
}
// 任务已存在并且表达式未发生变化则跳过
if (scheduledFutures.containsKey(item.getTaskId()) && cronTasks.get(item.getTaskId()).getExpression().equals(expression)) {
return;
}
// 任务执行时间发生了变化,则删除该任务
if (scheduledFutures.containsKey(item.getTaskId())) {
scheduledFutures.get(item.getTaskId()).cancel(false);
scheduledFutures.remove(item.getTaskId());
cronTasks.remove(item.getTaskId());
}
CronTask task = new CronTask(new Runnable() {
@Override
public void run() {
// 执行业务逻辑
try {
log.info("====执行单个任务,任务ID【{}】执行规则【{}】=======", item.getTaskId(), item.getExpression());
taskContextService.execute(item.getCode());
} catch (Exception e) {
log.error("执行任务异常,异常信息:{}", e);
}
}
}, expression);
ScheduledFuture<?> future = registrar.getScheduler().schedule(task.getRunnable(), task.getTrigger());
cronTasks.put(item.getTaskId(), task);
scheduledFutures.put(item.getTaskId(), future);
});
}
}
任务自启动配置
启动项目是读取任务配置表中的信息,初始化任务执行列表。
/**
* @ClassName: TaskApplicationRunner.java
* @ClassPath: com.tansci.common.task.TaskApplicationRunner.java
* @Description: 任务自启动配置
* @Author: tanyp
* @Date: 2022/2/25 9:43
**/
@Slf4j
@Component
public class TaskApplicationRunner implements ApplicationRunner {
@Autowired
private ScheduledTask scheduledTask;
@Autowired
private TaskConfigService taskConfigService;
@Override
public void run(ApplicationArguments args) {
try {
log.info("================项目启动初始化定时任务====开始===========");
List<TaskConfig> tasks = taskConfigService.list(Wrappers.<TaskConfig>lambdaQuery().eq(TaskConfig::getStatus, 1));
log.info("========初始化定时任务数为:{}=========", tasks.size());
scheduledTask.refreshTask(tasks);
log.info("================项目启动初始化定时任务====完成==========");
} catch (Exception e) {
log.error("================项目启动初始化定时任务====异常:{}", e);
}
}
}
任务配置相关类
- TaskConfig: 任务配置实体
- TaskConfigService: 接口
- TaskConfigServiceImpl: 接口实现类
- TaskConfigMapper: Mapper 接口
/**
* @ClassName: TaskConfig.java
* @ClassPath: com.tansci.domain.system.TaskConfig.java
* @Description: 任务配置
* @Author: tanyp
* @Date: 2022/2/25 9:35
**/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "task_config")
@ApiModel(value = "任务配置")
public class TaskConfig {
@ApiModelProperty(value = "主键id")
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@ApiModelProperty(value = "任务服务名称")
private String code;
@ApiModelProperty(value = "任务编码")
private String taskId;
@ApiModelProperty(value = "任务执行规则时间:cron表达式")
private String expression;
@ApiModelProperty(value = "任务名称")
private String name;
@ApiModelProperty(value = "状态:0、未启动,1、正常")
private Integer status;
@ApiModelProperty(value = "状态")
@TableField(exist = false)
private String statusName;
@ApiModelProperty(value = "创建人")
private String creater;
@ApiModelProperty(value = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private LocalDateTime updateTime;
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private LocalDateTime createTime;
@ApiModelProperty(value = "描述")
private String remarks;
}
动态调用任务配置
通过 task_config 配置的 code 字段信息来调用业务实现 spring bean。
/**
* @ClassName: TaskContextServiceImpl.java
* @ClassPath: com.tansci.service.impl.system.TaskContextServiceImpl.java
* @Description: 动态调用任务配置信息
* @Author: tanyp
* @Date: 2022/2/25 10:12
**/
@Slf4j
@Service
public class TaskContextServiceImpl implements TaskContextService {
/**
* 任务注册器
*/
@Autowired
private Map<String, TaskRegisterService> componentServices;
/**
* @MonthName: execute
* @Description: 解析器
* @Author: tanyp
* @Date: 2022/2/25 10:13
* @Param: [taskServerName]
* @return: void
**/
@Override
public void execute(String taskServerName) {
componentServices.get(taskServerName).register();
}
}
任务注册器
/**
* @ClassName: TaskRegisterService.java
* @ClassPath: com.tansci.service.system.TaskRegisterService.java
* @Description: 任务注册器
* @Author: tanyp
* @Date: 2022/2/25 10:05
**/
public interface TaskRegisterService {
void register();
}
创建业务实现时,只需实现 TaskRegisterService 接口即可。
注意:
@Service("taskTest1Service") 是唯一的,对应 task_config 表中的 code 字段;
expression 的配置为 cron 表达式。
创建两个任务测试类:
@Slf4j
@Service("taskTest1Service")
public class TaskTest1ServiceImpl implements TaskRegisterService {
@Override
public void register() {
log.info("===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========");
}
}
@Slf4j
@Service("taskTest2Service")
public class TaskTest2ServiceImpl implements TaskRegisterService {
@Override
public void register() {
log.info("===========自定义任务测试【TaskTest2ServiceImpl】====【3】=========");
}
}
三、测试
在界面配置 taskTest1Service、taskTest2Service 如下:


启动项目,执行结果如下:
2022-02-25 12:59:00,007 [pool-2-thread-1] INFO com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
2022-02-25 12:59:00,007 [pool-2-thread-1] INFO com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
2022-02-25 12:59:20,015 [pool-2-thread-3] INFO com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
2022-02-25 12:59:20,015 [pool-2-thread-3] INFO com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
2022-02-25 12:59:40,004 [pool-2-thread-3] INFO com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
2022-02-25 12:59:40,004 [pool-2-thread-3] INFO com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
可以看到初始化的任务都在执行,并且是多线程在执行。
四、cron 表达式
corn 从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
| 字段 | 允许值 | 允许的特殊字符 |
|---|---|---|
| 秒(Seconds) | 0~59 的整数 | , - * / |
| 分(Minutes) | 0~59 的整数 | , - * / |
| 小时(Hours) | 0~23 的整数 | , - * / |
| 日期(DayofMonth) | 1~31 的整数 | ,- * ? / L W C |
| 月份(Month) | 1~12 的整数或者 JAN-DEC | , - * / |
| 星期(DayofWeek) | 1~7 的整数或者 SUN-SAT (1=SUN) | , - * ? / L C # |
| 年(可选,留空)(Year) | 1970~2099 | , - * / |
*:表示匹配该域的任意值。假如在 Minutes 域使用*, 即表示每分钟都会触发事件。?:只能用在 DayofMonth 和 DayofWeek 两个域。-:表示范围。例如在 Minutes 域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次/:表示起始时间开始触发,然后每隔固定时间触发一次。,:表示列出枚举值。例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分每分钟触发一次。L:表示最后,只能出现在 DayofWeek 和 DayofMonth 域。W:表示有效工作日(周一到周五),只能出现在 DayofMonth 域,系统将在离指定日期的最近的有效工作日触发事件。LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。#:用于确定每个月第几个星期几,只能出现在 DayofMonth 域。例如在 4#2,表示某月的第二个星期三。
常用表达式例子
0 0 2 1 * ? *表示在每月的 1 日的凌晨 2 点调整任务0 15 10 ? * MON-FRI表示周一到周五每天上午 10:15 执行作业0 15 10 ? 6L 2002-2006表示 2002-2006 年的每个月的最后一个星期五上午 10:15 执行作0 0 10,14,16 * * ?每天上午 10 点,下午 2 点,4 点0 0/30 9-17 * * ?朝九晚五工作时间内每半小时0 0 12 ? * WED表示每个星期三中午 12 点0 0 12 * * ?每天中午 12 点触发0 15 10 ? * *每天上午 10:15 触发0 15 10 * * ?每天上午 10:15 触发0 15 10 * * ? *每天上午 10:15 触发0 15 10 * * ? 20052005 年的每天上午 10:15 触发0 * 14 * * ?在每天下午 2 点到下午 2:59 期间的每 1 分钟触发0 0/5 14 * * ?在每天下午 2 点到下午 2:55 期间的每 5 分钟触发0 0/5 14,18 * * ?在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发0 0-5 14 * * ?在每天下午 2 点到下午 2:05 期间的每 1 分钟触发0 10,44 14 ? 3 WED每年三月的星期三的下午 2:10 和 2:44 触发0 15 10 ? * MON-FRI周一至周五的上午 10:15 触发0 15 10 15 * ?每月 15 日上午 10:15 触发0 15 10 L * ?每月最后一日的上午 10:15 触发0 15 10 ? * 6L每月的最后一个星期五上午 10:15 触发0 15 10 ? * 6L 2002-20052002 年至 2005 年的每月的最后一个星期五上午 10:15 触发0 15 10 ? * 6#3每月的第三个星期五上午 10:1 触发
五、动态使用方式
1、启动方式有两种
- 启动项目后,手动调用
ScheduledTask.refreshTask(List<MyTask> tasks),并初始化任务列表; - 使用我测试中的方式,配置项目启动完成后自动调用初始任务的方法,并初始化任务列表。
2、数据初始化
只需要给 List<MyTask> 集合赋值并调用 refreshTask() 方法即可:
- 根据业务需求修改 TaskConfig 实体类;
- 这里的初始化数据可以从数据库读取数据赋值给集合;
例如:从 mysql 读取任务配置表的数据,调用
refreshTask()方法。
3、如何动态
- 修改:修改某一项正在执行的任务规则;
- 添加:添加一项新的任务;
- 删除:停止某一项正在执行的任务。
例如:我们有一张任务配置表,此时进行分别新增一条或多条数据、删除一条或多条数据、改一条数据,只需要完成以上任何一项操作后,重新调用一下
refreshTask()方法即可。
怎么重新调用 refreshTask() 方法:可以另外启一个任务实时监控任务表的数据变化。
SpringBoot 动态多线程并发定时任务的更多相关文章
- SpringBoot—自定义线程池及并发定时任务模板
介绍 在项目开发中,经常遇到定时任务,今天通过自定义多线程池总结一下SpringBoot默认实现的定时任务机制. 定时任务模板 pom依赖 <dependencies> <dep ...
- Java面试题整理一(侧重多线程并发)
1..是否可以在static环境中访问非static变量? 答:static变量在Java中是属于类的,它在所有的实例中的值是一样的.当类被Java虚拟机载入的时候,会对static变量进行初始化.如 ...
- Java多线程并发08——锁在Java中的应用
前两篇文章中,为各位带来了,锁的类型及锁在Java中的实现.接下来本文将为各位带来锁在Java中的应用相关知识.关注我的公众号「Java面典」了解更多 Java 相关知识点. 锁在Java中主要应用还 ...
- Cocos2d-x优化中多线程并发访问
多线程并发访问在Cocos2d-x引擎中用的不是很多,这主要是因为中整个结构设计没有采用多线程.源自于Objective-C的Ref对象,需要使用AutoreleasePool进行内存管理,Autor ...
- Cocos2d-x优化中多线程并发訪问
多线程并发訪问在Cocos2d-x引擎中用的不是非常多,这主要是由于中整个结构设计没有採用多线程. 源自于Objective-C的Ref对象,须要使用AutoreleasePool进行内存管理,Aut ...
- 多线程并发同一个表问题(li)
现有数据库开发过程中对事务的控制.事务锁.行锁.表锁的发现缺乏必要的方法和手段,通过以下手段可以丰富我们处理开发过程中处理锁问题的方法.For Update和For Update of使用户能够锁定指 ...
- Java多线程-并发容器
Java多线程-并发容器 在Java1.5之后,通过几个并发容器类来改进同步容器类,同步容器类是通过将容器的状态串行访问,从而实现它们的线程安全的,这样做会消弱了并发性,当多个线程并发的竞争容器锁的时 ...
- HashMap多线程并发问题分析
转载: HashMap多线程并发问题分析 并发问题的症状 多线程put后可能导致get死循环 从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题. ...
- 用读写锁三句代码解决多线程并发写入文件 z
C#使用读写锁三句代码简单解决多线程并发写入文件时提示“文件正在由另一进程使用,因此该进程无法访问此文件”的问题 在开发程序的过程中,难免少不了写入错误日志这个关键功能.实现这个功能,可以选择使用第三 ...
- WebDriver多线程并发
要想多线程并发的运行WebDriver,必须同时满足2个条件,首先你的测试程序是多线程,其次需要用到Selenium Server.下载位置如下图: 下载下来后是一个jar包,需要在命令行中运行.里面 ...
随机推荐
- springboot实现反向代理,动态代理目标地址
网上找了很多文章,各种照搬,只能自己实现 基于开源项目HTTP-Proxy-Servlet实现 开源项目地址:https://github.com/mitre/HTTP-Proxy-Servlet 1 ...
- [CF1601C] Optimal Insertion
Optimal Insertion 题面翻译 题目大意 给定两个序列 \(a,b\),长度分别为 \(n,m(1\leq n,m\leq 10^6)\).接下来将 \(b\) 中的所有元素以任意方式插 ...
- finally中的代码一定会执行吗?
通常在面试中,只要是疑问句一般答案都是"否定"的,因为如果是"确定"和"正常"的,那面试官就没有必要再问了嘛,而今天这道题的答案也是符合这个 ...
- setup的执行时机
setup是在beforeCreate之前执行的,也就是vue实例还未被创建,因为setup中并没有this指针 <script> export default { setup() { c ...
- java方法的定义与执行
java中的方法在类中定义. 定义方法格式: 访问修饰符 返回值类型 方法名(参数列表){ ... 执行内容 ... return 返回值; } 访问修饰符:表示方法在哪里能被 ...
- Educational Codeforces Round 159 总结
最失败的一集. C 开题顺序搞错,不小心先开了C,以为是A.还好C不难. 题意大概是在给定的数组最后添一个数(所有数两两不同),再自定义一个数 \(k\) ,数组中每个数可以加上若干个 \(k\) , ...
- 构建 dotnet&vue 应用镜像->推送到 Nexus 仓库->部署为 k8s 服务实践
前言 前面分享了 k8s 的部署安装,本篇来点实操,将会把一个 .net core + vue 的项目(zhontai),打包构建成 docker 镜像,推送到 nexus 镜像仓库,并部署到 k8s ...
- 学会@ConfigurationProperties月薪过三千
学习 @ConfigurationProperties 之前我们需要一些前置知识点: @Value是个什么东西 首先明确:@ConfigurationProperties 是 SpringBoot 注 ...
- Windows手工入侵排查思路
文章来源公众号:Bypass Windows系统被入侵后,通常会导致系统资源占用过高.异常端口和进程.可疑的账号或文件等,给业务系统带来不稳定等诸多问题.一些病毒木马会随着计算机启动而启动并获取一定的 ...
- Luogu P4592 [TJOI2018]异或 做题记录
随机跳的. 树上维护序列,显然树剖.维护异或,显然 01trie. 01trie 维护区间异或,显然可持久化一下. 看到时限很大,显然可以双 log. 于是跑一边树剖,再根据 id 暴力建一个 可持久 ...