一、定时任务的基础认知与演进思路
在 springboot 中,定时任务的发展大致经历了三个阶段:
静态阶段:使用
@scheduled注解硬编码 cron 表达式,改动需重启。动态阶段:通过
schedulingconfigurer接口从数据库读取 cron,实现表达式动态生效 。可控制阶段:使用
threadpooltaskscheduler或 quartz,不仅能改时间,还能手动启停任务 。
本文将覆盖上述所有阶段,其中 方案二 和 方案三 是目前中小团队最常用的“动态任务”解决方案。
二、方案一:基于注解的静态定时任务(快速入门)
作为基础铺垫,我们先快速搭建一个最简版本。
2.1 启用定时任务
在启动类上添加 @enablescheduling 注解。
@springbootapplication
@enablescheduling
public class scheduledapplication {
public static void main(string[] args) {
springapplication.run(scheduledapplication.class, args);
}
}2.2 创建静态任务
@component
public class statictask {
// 每5秒执行一次
@scheduled(cron = "0/5 * * * * ?")
public void execute() {
system.out.println("静态任务执行:" + localdatetime.now());
}
}2.3 优缺点
优点:代码简单,适合固定不变的周期性任务。
缺点:cron 表达式硬编码在代码中,修改需重启服务。
三、方案二:实现schedulingconfigurer接口(动态读取 cron)
这是实现“动态定时任务”最经典的方案,无需引入第三方依赖,直接从数据库读取 cron 表达式 。
3.1 核心原理
schedulingconfigurer 接口允许我们自定义 scheduledtaskregistrar,通过 addtriggertask 方法动态添加任务。每次执行任务前,都会重新读取数据库中的 cron 表达式,从而实现“动态生效”。
3.2 数据库表设计
create table `cron_config` ( `id` int not null auto_increment, `task_key` varchar(50) not null comment '任务唯一标识', `cron_expression` varchar(50) not null comment 'cron表达式', `task_desc` varchar(100) default null comment '任务描述', `status` tinyint default '1' comment '1:启用 0:停用', `updated_time` timestamp null default current_timestamp on update current_timestamp, primary key (`id`), unique key `uniq_task_key` (`task_key`) ) engine=innodb default charset=utf8mb4; insert into `cron_config` values (1, 'demotask', '0/10 * * * * ?', '演示任务', 1, now());
3.3 完整实现代码
3.3.1 实体类
@data
@tablename("cron_config")
public class cronconfig {
private integer id;
private string taskkey;
private string cronexpression;
private string taskdesc;
private integer status;
private localdatetime updatedtime;
}3.3.2 mapper 接口
@mapper
public interface cronconfigmapper extends basemapper<cronconfig> {
// 根据任务key查询有效的cron
default string getcronbykey(string taskkey) {
lambdaquerywrapper<cronconfig> wrapper = new lambdaquerywrapper<>();
wrapper.eq(cronconfig::gettaskkey, taskkey)
.eq(cronconfig::getstatus, 1);
cronconfig config = selectone(wrapper);
return config != null ? config.getcronexpression() : null;
}
}3.3.3 核心调度类
@component
public class dynamicschedulingtask implements schedulingconfigurer {
@autowired
private cronconfigmapper cronconfigmapper;
@override
public void configuretasks(scheduledtaskregistrar taskregistrar) {
// 注册一个trigger任务
taskregistrar.addtriggertask(
// 1. 任务内容 (runnable)
() -> executetask(),
// 2. 触发条件 (trigger)
triggercontext -> {
// 每次执行前都去数据库获取最新的cron
string cron = cronconfigmapper.getcronbykey("demotask");
if (stringutils.isempty(cron)) {
return null; // 如果没有配置或停用,则不执行
}
return new crontrigger(cron).nextexecutiontime(triggercontext);
}
);
}
private void executetask() {
system.out.println("动态任务执行: " + localdatetime.now());
// 这里可以写具体的业务逻辑
}
}3.4 测试动态修改
修改数据库中的 cron_expression 字段,例如从 0/10 * * * * ? 改为 0/20 * * * * ?,无需重启应用,下次任务执行时将自动采用新表达式 。
3.5 局限性分析
优点:无需引入额外依赖,实现简单,cron 修改即时生效。
缺点:无法手动停止任务(除非设置 cron 为 null),也无法在运行时新增任务。
四、方案三:使用threadpooltaskscheduler(手动控制启停)
如果需要通过 api 动态地启动、停止任务,或者支持一次性任务(如指定某时间执行一次),方案二就不够用了。此时需要使用 threadpooltaskscheduler 。
4.1 核心原理
threadpooltaskscheduler 是 spring 提供的任务调度线程池,它的 schedule(runnable task, trigger trigger) 方法可以动态提交任务,并返回一个 scheduledfuture 对象。
通过 scheduledfuture.cancel() 我们可以随时停止任务。
4.2 配置线程池
@configuration
public class schedulerconfig {
@bean
public threadpooltaskscheduler threadpooltaskscheduler() {
threadpooltaskscheduler scheduler = new threadpooltaskscheduler();
scheduler.setpoolsize(10); // 线程池大小
scheduler.setthreadnameprefix("dynamic-task-");
scheduler.setawaitterminationseconds(60); // 优雅停机等待时间
scheduler.setwaitfortaskstocompleteonshutdown(true); // 等待任务完成再停机
return scheduler;
}
}4.3 任务管理服务
这是一个核心服务类,支持任务的增、删、改、查。
@service
public class dynamictaskservice {
// 存储正在运行的任务
private final map<string, scheduledfuture<?>> taskmap = new concurrenthashmap<>();
@autowired
private threadpooltaskscheduler taskscheduler;
/**
* 启动一个cron任务
* @param taskid 任务id
* @param task 要执行的任务
* @param cronexpression cron表达式
*/
public boolean startcrontask(string taskid, runnable task, string cronexpression) {
// 如果任务已存在,先停止
if (taskmap.containskey(taskid)) {
stoptask(taskid);
}
scheduledfuture<?> future = taskscheduler.schedule(task, new crontrigger(cronexpression));
taskmap.put(taskid, future);
return true;
}
/**
* 启动一个一次性任务(指定时间执行)
* @param taskid 任务id
* @param task 任务
* @param starttime 执行时间
*/
public boolean startonetimetask(string taskid, runnable task, date starttime) {
if (taskmap.containskey(taskid)) {
stoptask(taskid);
}
scheduledfuture<?> future = taskscheduler.schedule(task, starttime);
taskmap.put(taskid, future);
return true;
}
/**
* 停止任务
*/
public boolean stoptask(string taskid) {
scheduledfuture<?> future = taskmap.get(taskid);
if (future != null) {
future.cancel(true); // 如果正在执行,是否立即中断
taskmap.remove(taskid);
return true;
}
return false;
}
/**
* 修改cron表达式(先停后启)
*/
public boolean updatecrontask(string taskid, runnable task, string newcron) {
stoptask(taskid);
return startcrontask(taskid, task, newcron);
}
public set<string> getrunningtasks() {
return taskmap.keyset();
}
}4.4 通过 controller 动态管理
@restcontroller
@requestmapping("/task")
public class taskcontroller {
@autowired
private dynamictaskservice dynamictaskservice;
// 启动一个cron任务
@postmapping("/start/cron")
public string startcron(@requestparam string taskid, @requestparam string cron) {
runnable task = () -> {
system.out.println(taskid + " 执行: " + localdatetime.now());
// 业务逻辑
};
dynamictaskservice.startcrontask(taskid, task, cron);
return "任务 " + taskid + " 已启动,cron: " + cron;
}
// 启动一次性任务
@postmapping("/start/once")
public string startonce(@requestparam string taskid,
@requestparam string time) throws parseexception {
simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss");
date starttime = sdf.parse(time);
runnable task = () -> system.out.println(taskid + " 一次性任务执行: " + localdatetime.now());
dynamictaskservice.startonetimetask(taskid, task, starttime);
return "任务 " + taskid + " 将在 " + time + " 执行";
}
// 停止任务
@deletemapping("/stop/{taskid}")
public string stop(@pathvariable string taskid) {
boolean b = dynamictaskservice.stoptask(taskid);
return b ? "任务已停止" : "任务不存在";
}
// 查看正在运行的任务
@getmapping("/list")
public set<string> list() {
return dynamictaskservice.getrunningtasks();
}
}4.5 与数据库结合
在实际生产中,我们不会通过 controller 直接硬编码任务内容,而是将任务信息存储在数据库(如任务名、执行类路径、cron、状态等)。启动时从数据库加载所有状态为“启用”的任务,调用 startcrontask 提交到调度器 。
4.6 优缺点
优点:灵活可控,支持运行时动态启停,支持一次性任务。
缺点:如果应用重启,内存中的任务会丢失,需要结合数据库做启动时的任务恢复。
五、方案四:整合 quartz 集群模式(企业级分布式方案)
当应用部署在多台服务器上时,需要避免任务被多次执行。此时需要引入分布式调度框架,如 quartz 集群、elastic-job 或 xxl-job 。
5.1 为什么需要 quartz?
持久化:任务和触发器信息存储在数据库表中,重启不丢失。
集群支持:通过数据库行锁实现任务分配的负载均衡和故障转移,确保同一个任务在同一时间只被一台机器执行 。
5.2 引入依赖
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-quartz</artifactid>
</dependency>
<dependency>
<groupid>mysql</groupid>
<artifactid>mysql-connector-java</artifactid>
</dependency>5.3 配置文件
spring:
quartz:
job-store-type: jdbc # 使用数据库存储
jdbc:
initialize-schema: never # 生产环境建议手动初始化,不要自动建表
properties:
org:
quartz:
scheduler:
instancename: clusteredscheduler
instanceid: auto # 集群中自动生成唯一id
jobstore:
class: org.quartz.impl.jdbcjobstore.jobstoretx
driverdelegateclass: org.quartz.impl.jdbcjobstore.stdjdbcdelegate
tableprefix: qrtz_
isclustered: true # 启用集群
clustercheckininterval: 10000
useproperties: false
threadpool:
class: org.quartz.simpl.simplethreadpool
threadcount: 10
threadpriority: 55.4 自定义业务表与 quartz 联动
在实际业务中,我们通常有一个自己的任务配置表(如 business_job)。应用启动时,需要从业务表读取任务,并注册到 quartz 中 。
5.4.1 业务表实体
@entity
@data
public class businessjob {
@id
private string jobname;
private string jobgroup;
private string cronexpression;
private string jobclass; // 全限定类名
private string jobdata; // json格式的参数
private integer status; // 1启用 0停用
}5.4.2 启动时加载任务
@component
public class jobinitlistener implements applicationlistener<contextrefreshedevent> {
@autowired
private scheduler scheduler;
@autowired
private businessjobrepository jobrepository;
@override
public void onapplicationevent(contextrefreshedevent event) {
try {
// 查询所有启用的任务
list<businessjob> joblist = jobrepository.findbystatus(1);
for (businessjob job : joblist) {
schedulejob(job);
}
} catch (exception e) {
e.printstacktrace();
}
}
private void schedulejob(businessjob job) throws exception {
// 构建jobdetail
jobdetail jobdetail = jobbuilder.newjob((class<? extends job>) class.forname(job.getjobclass()))
.withidentity(job.getjobname(), job.getjobgroup())
.storedurably()
.build();
// 设置jobdatamap(传递参数)
if (stringutils.isnotblank(job.getjobdata())) {
jobdetail.getjobdatamap().put("data", job.getjobdata());
}
// 构建trigger
crontrigger trigger = triggerbuilder.newtrigger()
.withidentity(job.getjobname() + "trigger", job.getjobgroup())
.withschedule(cronschedulebuilder.cronschedule(job.getcronexpression()))
.build();
// 调度
scheduler.schedulejob(jobdetail, trigger);
}
}5.5 任务执行类
public class samplejob implements job {
@override
public void execute(jobexecutioncontext context) {
string data = context.getmergedjobdatamap().getstring("data");
system.out.println("samplejob 执行,参数: " + data + ",时间: " + localdatetime.now());
}
}5.6 优缺点
优点:支持持久化、集群、故障转移,功能强大。
缺点:配置相对复杂,需要维护 quartz 的 11 张系统表。
六、四种方案对比与选型建议
| 特性 | 方案一 (@scheduled) | 方案二 (schedulingconfigurer) | 方案三 (threadpooltaskscheduler) | 方案四 (quartz集群) |
|---|---|---|---|---|
| 动态cron | 不支持 | 支持 | 支持 | 支持 |
| 动态启停 | 不支持 | 不支持 | 支持 | 支持 |
| 运行时新增任务 | 不支持 | 不支持 | 支持 | 支持 |
| 持久化 | 内存 | 内存 | 内存 | 数据库 |
| 分布式支持 | 不支持 | 不支持 | 不支持 | 支持 |
| 复杂性 | 低 | 中 | 中高 | 高 |
选型建议
个人项目 / 极简工具:方案一就够了。
中小项目,只有cron需动态配置,无集群需求:方案二最合适,轻量无侵入。
需要提供api给用户手动启停任务,或有一次性任务需求:方案三是首选。
大型分布式项目,必须保证任务不重复执行:直接上 quartz 或更专业的分布式调度平台(如 xxl-job)。
七、生产环境中的避坑指南
7.1 关于线程池
默认线程池大小为1:如果不自定义
taskscheduler,spring 默认使用单线程池执行所有定时任务。如果一个任务卡死,会导致所有任务阻塞。建议在配置类中设置合理的线程池大小 。
@bean
public taskscheduler taskscheduler() {
threadpooltaskscheduler scheduler = new threadpooltaskscheduler();
scheduler.setpoolsize(10);
return scheduler;
}7.2 关于分布式锁
如果暂时不想引入 quartz,但又有两台机器需要避免任务重复,可以使用 redis 分布式锁 + 方案二/方案三:
// 任务执行时尝试加锁
if (redistemplate.opsforvalue().setifabsent("task_lock", "1", 30, timeunit.seconds)) {
try {
// 执行业务逻辑
} finally {
redistemplate.delete("task_lock");
}
}7.3 关于任务幂等性
无论是否使用分布式调度,都应保证任务逻辑的幂等性,防止意外重复执行造成数据问题。
7.4 关于异常处理
定时任务中的异常如果不捕获,会导致任务后续不再执行(取决于调度器策略)。建议在任务最外层使用 try-catch 记录日志 。
八、总结与展望
本文详细介绍了 springboot 中实现动态定时任务的四种方式,从最简单的注解方式,到基于接口的cron动态刷新,再到灵活可控的 threadpooltaskscheduler,最后是企业级的 quartz 集群方案。
动态定时任务的核心逻辑始终是:将变化的配置(cron、状态)外移到数据库,在任务调度时实时读取。 掌握这些思路,足以应对大部分业务场景。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论