—— 目录 ——
1. 需求产生背景
在项目开发中过程中,遇到了一个根据用户动态添加的日程开始时间,在该时间来临前的前半个小时给予消息提醒的需求
第一时间就想到了定时任务调度,纠结了很久最后得出这套解决方案
总共经历了三个阶段,前俩个阶段在网上是有很多资源的,最后一个阶段是在参考了某位大神之后,自己构建出来的,如下:
阶段一:固定重复定时任务
我们知道 Spring 中有一个 @Scheduled
注解,可以使用 cron
表达式实现任务定时重复执行(如每天零点执行一次),但这种方式的缺点也很明显,那就是直接在程序中写死,想要更改必须重启程序
阶段二:动态重复定时任务
而如果能够根据数据库中定义的 cron
表达式,动态改变重复执行的规则,就灵活很多了,虽然这种方法还满足不了我们的需求,但却实实在在地为下一个阶段做好了铺垫
阶段三:动态时间点定时任务
在一些情况下(如本文的需求),任务并不是定时重复执行的,而是由用户或系统生成一个一个附带时间点的任务,希望程序在规定的时间点时执行相应的任务
感谢大神:springboot动态增加删除定时任务
2. 实现思路
在阶段二中,我们可以通过修改数据库中的 cron
表达式,完成定时任务规则的修改
而 cron
表达式与时间点是可以互相转化的(周和月只能同时存在一个,取月而周不确定)
如 2022-1-31 09:30:00
可以转化为 00 30 09 31 1 ?
(秒 分 时 天 月 周)
这样下来就实现了通过读取时间点来执行任务了
但由于没有书写重复执行,故该表达式执行完一次定时任务后,就不会再执行了
这时候就需要获取下一个最近的时间点,解析成 cron
表达式,来完成按照时间点序列先后执行定时任务
下一个问题是:时间点全部执行完了,但可能会有新的时间点加入
这时候就需要动态监听数据库的变化了,这里产生了两种思路:
① 使用 AOP,在插入新记录时,刷新定时任务
② 在没有任务时,将 cron 设置为定时重复执行,不断轮询数据库,直到有新数据出现就更换 cron 规则
思路一比思路二更通用且节省资源,但思路二更容易实现,且能检测到我们手动往数据库中添加的数据,这里采用思路二
所以具体的实现思路就是:
- 在 springBoot 项目启动时,为每一个用户分配一个定时任务
- 在每一个定时任务中,读取出尚未发送通知的时间点最近的一条数据
- 解析其时间点为 cron 表达式,作为下一次定时任务执行的时间
- 执行完定时任务后,将该记录标识为已通知
- 然后继续读取下一条数据并解析为 cron 表达式和执行任务
- 直到没有任务了,进行轮询,有新数据就解析为新 cron 表达式
- 有新用户注册时,为新用户动态分配一个定时任务
3. 具体实现(实战)
① 示范建表(只想看逻辑的可以跳过)
实现上述逻辑有很多建表的方式,我这里这个只作为示例拉~
-- 定时任务表
CREATE TABLE t_task (
task_id INT AUTO_INCREMENT COMMENT'任务 id' PRIMARY KEY,
group_id INT DEFAULT 1 COMMENT'任务所属组别,未指定时为默认组别',
task_code INT COMMENT'任务代码',
task_time CHAR(19) COMMENT'任务执行时间',
task_run INT DEFAULT 0 COMMENT'任务执行标识',
create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT'任务创建时间'
);
-- 定时任务分组
CREATE TABLE t_group (
group_id INT AUTO_INCREMENT COMMENT '组别 id' PRIMARY KEY,
group_name VARCHAR(255) COMMENT '组别名称',
group_task INT DEFAULT 0 COMMENT '组别任务数',
create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT'组别创建时间'
);
-- 默认组别
INSERT INTO t_group(group_name) VALUE('defaultGroup');
这里建立了两张表,一张任务表和一张任务分组表
一个任务分组对应了一个调度任务,一组内可以有多个任务,各参数解析如下:
task_code
指定该定时任务具体需要执行哪个任务,即 Runnable 中的具体逻辑task_time
指定了任务将在什么时间点执行,即在 Trigger 中用它解析成 cron 表达式task_run
标识了任务的执行状态,状态的含义可以自定义,这里定义的是 0 为未执行,-1 为执行完毕,大于 0 的所有值可以标识任务的各种状态(复杂任务需要),这里就具体需求啦,一般 0 和 -1 两个状态已经够用了
用来测试的数据如下:
② 自定义任务调度器(重 - 参考与改造)
这里主要用到 add 和 remove 方法,增加和移除调度任务,规则由外部传入
具体的实现参照了前边那位大神(基本上全搬过来拉)
本质上和阶段二中使用的 taskRegistrar.addTriggerTask
是一样的,不过阶段二中这个没有提供移除任务的接口,所以该类拆开了一层封装,将实际执行任务的 SheduledFuture
提取出来了
往后我们只需要对其构成的集合进行增删,就可以实现任务的动态增加和移除了
@Component
public class MyScheduling implements SchedulingConfigurer {
/** 定时任务注册器 */
private ScheduledTaskRegistrar taskRegistrar;
private Set<ScheduledFuture<?>> scheduledFutures = null;
/** 存放所有定时任务 */
private Map<