一、引言
定时任务这玩意儿,选对了省心省力,选不对天天踩坑, 咱们今天就基于 SpringBoot 1.5.x 这个“老伙计”,把 Java 定时任务的选型门道唠明白,让你从此告别“定时任务为啥又没跑”的灵魂拷问。本文基于项目实践梳理主流方案及选型思路,其他版本的 SpringBoot 请按需调整依赖版本。
二、主流定时任务方案概览
2.1 方案分类
根据使用场景和复杂度,Java 定时任务方案可分为以下几类:
选型核心原则:能用简单的就别搞复杂的,能少依赖就少依赖。
| 方案类型(趣味副标题) | 代表技术 | 适用场景 | 复杂度 | 生产环境使用率 |
|---|---|---|---|---|
| JDK 原生(新手入门款,能用但不顶用) | Timer/TimerTask、ScheduledExecutorService | 单机简单任务 | ⭐ | Timer/TimerTask:<5%,纯玩具级;ScheduledExecutorService:≈15%,轻量场景 |
| Spring 框架(居家旅行款,简单又实用) | @Scheduled | 单机/集群(需配合分布式锁) | ⭐⭐ | ≈60%,中小型项目主力 |
| 企业级调度(专业选手款,功能全但费配置) | Quartz | 单机/集群、复杂调度 | ⭐⭐⭐ | ≈70%,传统企业项目常用 |
| 分布式调度(团队协作款,集群专属) | XXL-JOB、Elastic-Job | 分布式集群、任务管理 | ⭐⭐⭐⭐ | ≈50%,互联网与微服务项目常见 |
三、各方案详细介绍
3.1 JDK 原生方案
3.1.1 Timer/TimerTask —— “老古董”定时任务,单机简单活能凑活,生产用了准翻车
特点:
- JDK 1.3+ 提供,无需额外依赖
- 单线程干活就像一个人干十个人的活,一个任务卡壳,后面全歇菜
- 不支持 Cron 表达式
- 不抓异常的定时任务,就像走路没看路,摔一跤直接躺平,再也不干活了
代码示例:
import java.util.Timer;
import java.util.TimerTask;
/**
* Timer/TimerTask 示例
* 【规范校验】:符合 JDK 标准 API,但生产环境不推荐使用
* 【版本兼容】:JDK 1.3+
*/
public class TimerTaskExample {
public static void main(String[] args) {
Timer timer = new Timer("TimerTask-Thread", true);
// 延迟 1 秒执行,每 5 秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
System.out.println("执行定时任务: " + System.currentTimeMillis());
// 【优化】业务逻辑务必捕获异常,防止单线程直接挂掉
} catch (Exception ex) {
System.err.println("任务执行异常,已兜底捕获: " + ex.getMessage());
}
}
}, 1000, 5000);
}
}
适用场景:
- 简单的单次延迟任务
- 学习演示
- 非生产环境临时脚本
不推荐原因:
- 单线程干活,任务一堵就全堵,别指望它帮你顶事
- 异常不兜底,摔跤就躺平,半夜运维喊你起床修
- 功能单一,只适合写 demo 玩,生产真用?除非想体验被叫醒的快乐
3.1.2 ScheduledExecutorService —— JDK 亲儿子升级版,比 Timer 靠谱,但仍够不上生产级
特点:
- JDK 1.5+ 提供,基于线程池实现
- 支持多线程并发执行
- 异常不会导致调度停止
- 功能相对完善,但仍不支持 Cron 表达式
代码示例:
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ScheduledExecutorService 示例
* 【规范校验-优化】:符合《阿里巴巴 Java 开发手册》线程池使用规范,线程池参数显式可控
* 【版本兼容】:JDK 1.5+
*/
public class ScheduledExecutorExample {
private static final ScheduledExecutorService scheduler =
new ScheduledThreadPoolExecutor(
4,
new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("sched-pool-" + seq.getAndIncrement());
t.setDaemon(false);
return t;
}
},
// 【规范校验-优化】拒绝策略明确,防止任务悄悄丢失
new ThreadPoolExecutor.CallerRunsPolicy()
);
public static void main(String[] args) {
// 延迟 1 秒执行,每 5 秒执行一次
scheduler.scheduleAtFixedRate(() -> {
System.out.println("执行定时任务: " + System.currentTimeMillis());
}, 1, 5, TimeUnit.SECONDS);
}
/**
* 优雅关闭线程池
*/
public static void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// 【优化】兜底校验,若仍未终止则再次尝试关闭并传递中断标记
if (!scheduler.isTerminated()) {
scheduler.shutdownNow();
}
if (Thread.currentThread().isInterrupted()) {
Thread.currentThread().interrupt();
}
}
}
适用场景:
- 简单的周期性任务
- 不需要 Cron 表达式的场景
- 轻量级应用,适合小打小闹;要是想搞复杂调度,还得换家伙事儿
3.2 Spring @Scheduled 方案 —— Spring 全家桶标配,注解一贴就能用,集群得配锁
3.2.1 基本使用
特点:
- Spring 3.0+ 提供,与 Spring 框架深度集成
- 支持 Cron 表达式、固定延迟、固定频率
- 配置简单,注解驱动
- 单机部署 OK,集群部署下直接用?那恭喜你,多实例抢着干活,数据能给你干出花儿来,必须加分布式锁防“内卷”
代码示例:
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* Spring @Scheduled 定时任务示例
* 【规范校验】:符合 Spring 最佳实践,需配合 @EnableScheduling 启用
* 【版本兼容】:SpringBoot 1.5.x 完全支持,lombok 1.16.x 更稳;版本不对,@Slf4j 就像“哑巴”,日志打不出来还不报错,坑死人
*/
@Slf4j
@Component
public class SpringScheduledTask {
/**
* Cron 表达式:每 30 秒执行一次
* 格式:秒 分 时 日 月 周 [年]
* ⚠️ 不同框架对“周”的取值不一样:Quartz/Spring 中 1=周日、0=周日也常见,
* 就像有的地方管土豆叫马铃薯,先搞清楚叫法,别配错了跑偏。
*/
@Scheduled(cron = "0/30 * * * * ?")
public void executeShortFrequencyTask() {
log.info("短周期任务执行开始");
try {
// 业务逻辑
doBusiness();
} catch (OutOfMemoryError oom) {
// 【补充】Error 级别兜底,防止任务彻底崩溃
log.error("短周期任务发生 OOM,触发降级与告警", oom);
triggerAlarm("shortFrequencyTask-oom");
fallback();
} catch (Throwable e) {
log.error("短周期任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
}
log.info("短周期任务执行结束");
}
/**
* 固定延迟:上次执行完成后延迟 10 分钟执行
*/
@Scheduled(fixedDelay = 600_000)
public void executeFixedDelayTask() {
log.info("固定延迟任务执行开始");
try {
doBusiness();
} catch (OutOfMemoryError oom) {
// 【补充】Error 级别兜底
log.error("固定延迟任务发生 OOM,触发降级与告警", oom);
triggerAlarm("fixedDelayTask-oom");
fallback();
} catch (Throwable e) {
log.error("固定延迟任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
}
log.info("固定延迟任务执行结束");
}
/**
* 固定频率:每 10 分钟执行一次(不考虑上次执行时间)
*/
@Scheduled(fixedRate = 600_000)
public void executeFixedRateTask() {
log.info("固定频率任务执行开始");
try {
doBusiness();
} catch (OutOfMemoryError oom) {
log.error("固定频率任务发生 OOM,触发降级与告警", oom);
triggerAlarm("fixedRateTask-oom");
fallback();
} catch (Throwable e) {
log.error("固定频率任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
}
log.info("固定频率任务执行结束");
}
/**
* 初始延迟:应用启动后延迟 1 分钟执行,之后每 10 分钟执行一次
*/
@Scheduled(initialDelay = 60_000, fixedRate = 600_000)
public void executeWithInitialDelay() {
log.info("带初始延迟的任务执行开始");
try {
doBusiness();
} catch (OutOfMemoryError oom) {
log.error("带初始延迟任务发生 OOM,触发降级与告警", oom);
triggerAlarm("initialDelayTask-oom");
fallback();
} catch (Throwable e) {
log.error("带初始延迟任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
}
log.info("带初始延迟的任务执行结束");
}
private void doBusiness() {
// 业务逻辑实现
}
private void retryOrFallback(Throwable e) {
// 【补充】简单失败重试 + 降级示例,可对接重试组件 / MQ 补偿
// retryOnce();
fallback();
triggerAlarm("scheduledTask-failed");
}
private void fallback() {
// 降级逻辑,比如写本地缓存 / 打标记等待人工处理等
}
private void triggerAlarm(String scene) {
// 告警逻辑,比如接入短信、邮件、钉钉机器人等
log.warn("定时任务触发告警,scene={}", scene);
}
}
配置类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 定时任务配置类
* 【规范校验-优化】:使用手动配置的线程池替代默认单线程,核心数、最大数、队列、拒绝策略全部显式可控
* 【版本兼容】:SpringBoot 1.5.x 使用 SchedulingConfigurer 配置
*/
@Slf4j
@Configuration
@EnableScheduling
public class ScheduledTaskConfig implements SchedulingConfigurer {
private static final int CORE_POOL_SIZE = 4;
private static final int MAX_POOL_SIZE = 8;
private static final int QUEUE_CAPACITY = 1000;
private static final long KEEP_ALIVE_TIME = 60L;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
CORE_POOL_SIZE,
new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("scheduled-task-pool-" + seq.getAndIncrement());
t.setDaemon(false);
return t;
}
},
// 【规范校验-优化】拒绝策略:CallerRuns,防止任务静默丢失
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.setMaximumPoolSize(MAX_POOL_SIZE);
executor.setKeepAliveTime(KEEP_ALIVE_TIME, TimeUnit.SECONDS);
// 模拟队列容量(ScheduledThreadPoolExecutor 内部用 DelayedWorkQueue,这里仅作为示意)
// 如需显式有界队列,可改用 ThreadPoolTaskScheduler + 自定义线程池
// 【补充】简单监控指标埋点示例:定期打印队列大小与活跃线程数
executor.scheduleAtFixedRate(() -> {
log.info("【补充】scheduled pool metrics, activeCount={}, queueSize={}",
executor.getActiveCount(), executor.getQueue().size());
}, 60, 60, TimeUnit.SECONDS);
taskRegistrar.setScheduler(executor);
}
}
3.2.2 集群环境下的分布式锁方案 —— 集群防“内卷”:只让一个实例干活,其余的歇着
问题: 集群环境下不加锁,多个实例跟打了鸡血似的重复执行,数据同步、报表生成全乱套,需要配合分布式锁只让一个实例“上岗”。
基于 Redisson 的 看门狗 锁续期方案:
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 带分布式锁的定时任务(Redisson Watchdog 版本)
* 【规范校验-优化】:使用 Redisson 分布式锁 + 看门狗自动续期,防止长任务锁过期
* 【版本兼容】:SpringBoot 1.5.x + Redisson 3.x
*/
@Slf4j
@Component
public class DistributedScheduledTask {
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "scheduled:task:lock";
@Scheduled(cron = "0 0/10 * * * ?")
public void executeWithDistributedLock() {
RLock lock = redissonClient.getLock(LOCK_KEY);
boolean locked = false;
try {
// tryLock(waitTime, leaseTime, unit)
// leaseTime 传 -1 走 Redisson Watchdog 自动续期机制
locked = lock.tryLock(5, -1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("获取分布式锁被中断", e);
return;
}
if (!locked) {
log.info("未获取到分布式锁,跳过本次执行");
return;
}
try {
log.info("获取分布式锁成功,开始执行任务");
doBusiness();
} catch (OutOfMemoryError oom) {
log.error("分布式锁任务发生 OOM,触发降级与告警", oom);
triggerAlarm("distributedTask-oom");
fallback();
} catch (Throwable e) {
log.error("分布式锁任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("释放分布式锁");
}
}
}
private void doBusiness() {
// 业务逻辑实现
}
private void retryOrFallback(Throwable e) {
// 【补充】失败重试 + 降级逻辑,与 3.2.1 中保持一致
fallback();
triggerAlarm("distributedTask-failed");
}
private void fallback() {
// 降级逻辑实现
}
private void triggerAlarm(String scene) {
// 告警逻辑实现
log.warn("分布式定时任务触发告警,scene={}", scene);
}
}
基于 Lua 脚本的原子释放锁示例【补充】:
// Lua 脚本:只有当当前线程/实例持有的锁值与 Redis 中一致时才删除
// Lua 脚本保证原子性,就像上厕所锁门,要么锁上用完,要么没锁上不用,不会半道被抢。
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
适用场景:
- SpringBoot 项目中的简单定时任务
- 单机部署或集群环境(需配合分布式锁防“内卷”)
- 任务数量较少(< 50 个)
优缺点:
- ✅ 优点:优点贼明显:注解一贴就生效,不用额外配一堆东西,Spring 玩家一看就会
- ❌ 缺点:缺点也扎心:想动态改任务?没门;想监控任务状态?没戏;集群用还得自己折腾锁
版本适配与跨版本对比【补充】
-
SpringBoot 1.5.x 特殊注意点:
initialDelay/fixedRate等毫秒值如果写得特别大,在 1.5.x + JDK 低版本下可能存在整数溢出风险。 就好比把参数设成了“天文数字”,数值直接“爆表”,定时任务直接“罢工”,永远等不到执行。 建议:- 合理拆分大周期(例如“每天一次”拆成“每小时轮询 + 业务内判断”)
- 避免使用
Integer.MAX_VALUE级别的固定间隔
- Spring Data Redis 1.7.2 的
setIfAbsent等操作在高并发下性能一般,可以考虑迁移到基于 Lettuce 的客户端, 换个“新工具”干活更快,连接管理更高效。
-
SpringBoot 2.x 中 @Scheduled 的变化:
- 配置入口与 1.5.x 类似,但推荐使用
TaskScheduler/ThreadPoolTaskScheduler统一管理定时任务线程池。 - 一些默认配置与自动装配略有调整,例如
spring.task.scheduling.pool.size等属性可直接在配置文件中指定。 - 可以把 1.5 升到 2.x 理解成:手机从安卓 10 更到 13,操作入口变了、设置更细了,但“打电话发消息”这种核心功能还在, 只是线程池和监控的姿势更现代、更统一。
- 配置入口与 1.5.x 类似,但推荐使用
3.3 Quartz 方案 —— 企业级老炮,功能全到用不完,配置也复杂到头疼
3.3.1 基本使用
特点:
- 功能强大的企业级任务调度框架
- 任务信息存数据库,集群部署不慌,就算某个节点挂了,其他节点能接上,主打一个稳
- 支持任务动态添加、删除、修改
- 提供任务监听器、触发器监听器,扩展点多到你用不完
- 配置能绕晕新手,光 properties 文件就得配十几行,新手容易配错导致启动就报错
Maven 依赖(SpringBoot 1.5.x 兼容版本):
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>
说明: 版本选不对,代码两行泪。2.1.7 在 SpringBoot 1.5.x + 某些容器组合下,在线程池与 JDBC JobStore 上有一些诡异的兼容性坑(偶发性线程泄漏、调度器无法优雅关闭),升级到 2.2.3 相对稳一些,坑少一截。
代码示例:
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* Quartz 定时任务示例
* 【规范校验】:符合 Quartz 最佳实践,使用 JobDetail 和 Trigger 分离设计
* 【版本兼容-优化】:Quartz 2.2.3 更适配 SpringBoot 1.5.x,避开旧版本线程池兼容坑
*/
@Slf4j
@Component
public class QuartzTaskExample {
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@PostConstruct
public void init() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
// 定义 Job(可以通过 JobDataMap 传入一些业务参数)
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "group1")
.withDescription("示例定时任务")
.storeDurably()
.build();
// 定义 Trigger(每 30 秒执行一次)
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?"))
.build();
// 【补充】任务已存在时的重复添加检查
// 别给同一个任务重复“报名”,不然框架会“懵圈”
if (scheduler.checkExists(jobDetail.getKey())) {
log.warn("Job 已存在,跳过重复注册:key={}", jobDetail.getKey());
} else {
// 调度任务
scheduler.scheduleJob(jobDetail, trigger);
log.info("Quartz 任务注册成功");
}
}
/**
* 自定义 Job 实现
* 静态类想拿 Spring Bean?得走“特殊通道”:
* 1)在 SchedulerFactoryBean 中配置 SpringBeanJobFactory
* 2)或自定义 AutowiringSpringBeanJobFactory,把 Spring 容器塞进 Quartz
*/
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("Quartz 任务执行: {}", System.currentTimeMillis());
try {
// 业务逻辑
} catch (Exception e) {
// 【补充】失败重试的一个简单示例:抛出 JobExecutionException 控制是否立即重试
JobExecutionException ex = new JobExecutionException(e);
// true = 立即重试;这里示例用 false,实际可结合业务判断
ex.setRefireImmediately(false);
throw ex;
}
}
}
}
Quartz 配置类:
import org.quartz.SchedulerException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import javax.sql.DataSource;
import java.util.Properties;
/**
* Quartz 配置类
* 【规范校验】:支持数据库持久化,集群模式下使用 JDBC JobStore
* 【版本兼容】:SpringBoot 1.5.x 使用 SchedulerFactoryBean 配置
*/
@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) throws SchedulerException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
Properties properties = new Properties();
// 使用 JDBC JobStore(支持集群)
properties.put("org.quartz.jobStore.class",
"org.quartz.impl.jdbcjobstore.JobStoreTX");
properties.put("org.quartz.jobStore.driverDelegateClass",
"org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
properties.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
properties.put("org.quartz.jobStore.isClustered", "true");
properties.put("org.quartz.jobStore.clusterCheckinInterval", "20000");
// 线程池配置
properties.put("org.quartz.threadPool.class",
"org.quartz.simpl.SimpleThreadPool");
properties.put("org.quartz.threadPool.threadCount", "10");
properties.put("org.quartz.threadPool.threadPriority", "5");
// 【补充】集群模式下所有节点 quartz.properties 配置必须一致
// 就像开家庭会议,所有人拿到的“议题清单”得一模一样,
// 不然节点之间会“吵架”,任务调度直接乱套。
factory.setQuartzProperties(properties);
factory.setSchedulerName("MyScheduler");
factory.setStartupDelay(10);
factory.setApplicationContextSchedulerContextKey("applicationContext");
// 【补充】使用 SpringBeanJobFactory 让 Job 支持 Spring 注入
SpringBeanJobFactory jobFactory = new SpringBeanJobFactory();
factory.setJobFactory(jobFactory);
return factory;
}
}
适用场景:
- 需要任务持久化的场景
- 集群环境下的任务调度
- 需要动态管理任务的场景
- 复杂的调度需求(如任务依赖、任务链)
- 适合不差人手、愿意折腾配置的团队,小团队慎选——配置错了排查到怀疑人生
优缺点:
- ✅ 优点:功能贼拉全,你能想到的调度需求它都能满足,集群、持久化、动态管理全拿捏
- ❌ 缺点:配置比 @Scheduled 复杂 10 倍,还得依赖数据库,学习成本高,小项目用它纯属“杀鸡用牛刀”
3.4 分布式调度框架
3.4.1 XXL-JOB —— 分布式调度“网红款”,有 Web 界面,运维小姐姐都爱用
特点:
- 轻量级分布式任务调度平台
- 自带 Web 管理后台,任务增删改查、执行日志、监控告警全配齐,不用自己搭监控
- 支持任务分片、故障转移、失败重试
- 支持多种执行器(Java、Shell、Python 等)
版本与依赖【补充】:
- SpringBoot 1.5.x 推荐搭配:
xxl-job-admin 2.2.0+xxl-job-core 2.2.0 - 版本配对才能“愉快合作”,配错了就“闹脾气”(例如协议字段不兼容、心跳/回调接口变更等)
核心配置示例片段【补充】:
# xxl-job 执行器配置示例(application.properties)
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
xxl.job.accessToken=
xxl.job.executor.appname=hcare-job-executor
xxl.job.executor.address=
xxl.job.executor.ip=
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job
xxl.job.executor.logretentiondays=30
@Component
public class SampleXxlJob {
/**
* 简单示例任务
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB 示例任务执行开始");
// 业务逻辑
XxlJobHelper.log("XXL-JOB 示例任务执行结束");
}
}
适用场景:
- 分布式集群环境
- 需要任务管理和监控
- 多语言任务执行
- 适合想少折腾、要可视化管理的团队,中小厂首选
3.4.2 Elastic-Job —— 基于 Quartz 的分布式升级版,分片能力顶,但配置更复杂
特点:
- 基于 Quartz 的分布式调度解决方案
- 任务能拆成小块分给不同节点干,机器不够了直接加节点,自动分摊任务,主打一个弹性
- 提供作业治理功能(失效转移、错过执行修复、分片信息可视化等)
版本与依赖【补充】:
- SpringBoot 1.5.x 常见搭配:
elastic-job-lite 2.1.5 - 需要额外依赖注册中心(如 Zookeeper),对运维和基础设施要求更高
核心配置示例片段【补充】:
@Bean
public CoordinatorRegistryCenter registryCenter() {
ZookeeperConfiguration zkConfig =
new ZookeeperConfiguration("localhost:2181", "elastic-job-demo");
CoordinatorRegistryCenter registryCenter = new ZookeeperRegistryCenter(zkConfig);
registryCenter.init();
return registryCenter;
}
@Bean(initMethod = "init")
public JobScheduler demoJobScheduler(CoordinatorRegistryCenter registryCenter) {
LiteJobConfiguration jobConfig = LiteJobConfiguration
.newBuilder(
new SimpleJobConfiguration(
JobCoreConfiguration.newBuilder(
"demoElasticJob", "0/30 * * * * ?", 4)
.build(),
DemoElasticJob.class.getCanonicalName()
)
)
.overwrite(true)
.build();
return new SpringJobScheduler(new DemoElasticJob(), registryCenter, jobConfig);
}
public class DemoElasticJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
int item = shardingContext.getShardingItem();
// 根据分片项处理不同子任务
System.out.println("Elastic-Job 分片任务执行,item=" + item);
}
}
与 XXL-JOB 的选型对比【补充】:
- XXL-JOB:偏“平台化 + 可视化”,上手快、Web 管理界面友好,适合大多数中小团队;
- Elastic-Job:偏“框架 + 底层能力”,分片和扩展能力更细粒度,需要自己搭配注册中心和监控,适合大厂大规模集群,愿意投入成本做定制化的场景。
适用场景:
- 大规模分布式任务调度
- 需要任务分片和弹性扩容
- 对基础设施要求高、团队有一定中间件经验的场景
四、选择思路与决策树
4.1 决策流程图(带一点“人话”版)
4.2 选择维度对比表(顺便来点主观“吐槽”)
| 维度 | Timer | ScheduledExecutorService | @Scheduled | Quartz | XXL-JOB |
|---|---|---|---|---|---|
| 学习成本 | ⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 功能丰富度 | ⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 集群支持 | ❌ | ❌ | ⚠️(需手动加锁,锁写不好分分钟翻车) | ✅ | ✅ |
| 任务持久化 | ❌ | ❌ | ❌ | ✅ | ✅ |
| 动态管理 | ❌ | ❌ | ❌ | ✅ | ✅ |
| 监控管理 | ❌ | ❌ | ❌ | ⚠️(需要自己补监控 / 控制台) | ✅(开箱即用,Web 可视化) |
| Cron 支持 | ❌ | ❌ | ✅ | ✅ | ✅ |
| 依赖复杂度 | 无 | 无 | Spring | Quartz | XXL-JOB |
| 趣味评价 | 纯玩具,生产别碰 | 轻量线程池版闹钟,凑合用 | Spring 全家桶自带闹钟,够用就行 | 企业级老炮,强但难伺候 | 分布式调度“网红款”,可视化懒人福音 |
4.3 具体场景推荐
场景 1:简单单机定时任务
需求: 每天凌晨执行数据统计,单机部署 推荐方案: Spring @Scheduled 理由:
- 配置简单,注解驱动
- 与 SpringBoot 深度集成
- 无需额外依赖
- 每天凌晨跑个统计,就这点活,犯不着上 Quartz,
@Scheduled注解一贴,齐活!
性能小贴士:
- 单机任务数量在约 50 个以内、每个任务执行时间较短(< 1 秒)时,
@Scheduled+ 合理线程池基本无压力; - 如果任务开始上百、耗时又长,就该考虑拆分任务或升级到 Quartz / 分布式调度了。
代码示例:
@Scheduled(cron = "0 0 0 * * ?")
public void dailyStatistics() {
// 数据统计逻辑
}
场景 2:集群环境定时任务
需求: 每小时执行数据同步,多实例部署,不能重复执行
推荐方案: Spring @Scheduled + Redis 分布式锁 或 Quartz 集群模式
方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| @Scheduled + 分布式锁 | 实现简单、代码侵入小 | 需要维护锁逻辑、分布式锁实现成本需要评估(Redis/Redisson/Lua 脚本等) |
| Quartz 集群 | 框架级支持、功能完善 | 需要数据库、配置复杂,部署和排障成本更高 |
选择建议:
- 任务数量 < 20:使用 @Scheduled + 分布式锁,锁逻辑写稳一点就行;
- 任务数量 >= 20:使用 Quartz 集群模式,让框架帮你兜底;
性能小贴士:
@Scheduled + 分布式锁:适合几十个以内的集群任务,Redis 锁 QPS 在常规业务量下绰绰有余;- Quartz 集群:适合任务较多、执行频率较高的场景,但数据库 I/O 会变多,需要预留一点 DB 性能。
场景 3:需要动态管理的任务
需求: 任务需要根据业务动态添加、删除、修改
推荐方案: Quartz 或 XXL-JOB
代码示例(Quartz 动态管理):
@Service
public class DynamicTaskService {
@Autowired
private Scheduler scheduler;
/**
* 动态添加任务
*/
public void addJob(String jobName, String jobGroup, String cron)
throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(DynamicJob.class)
.withIdentity(jobName, jobGroup)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName + "Trigger", jobGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
/**
* 动态删除任务
*/
public void deleteJob(String jobName, String jobGroup)
throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
scheduler.deleteJob(jobKey);
}
/**
* 动态修改任务
*/
public void updateJob(String jobName, String jobGroup, String cron)
throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(
jobName + "Trigger", jobGroup);
Trigger newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
}
}
场景 4:大规模分布式任务调度
需求: 数百个定时任务,需要统一管理和监控
推荐方案: XXL-JOB 或 Elastic-JOB
理由:
- 提供 Web 管理界面
- 支持任务监控、日志查看
- 支持任务分片、故障转移
- 适合大规模任务管理
- 数百个任务还想靠人工脚本 + 运维同学手动兜底?别卷自己了,XXL-JOB / Elastic-Job 安排上,后台一键管控,香得很。
性能小贴士:
- 单个 XXL-JOB 执行器轻松支撑数百级任务调度,瓶颈更多在业务本身而非调度框架;
- Elastic-Job 依托分片 + 多节点扩容,理论上可以横向撑到上千任务,但前提是 ZK 和基础设施跟得上。
五、最佳实践与注意事项
5.1 SpringBoot 1.5.x 适配要点
5.1.1 线程池配置 —— 给定时任务配“专属司机”,别让所有任务挤一辆车
问题: Spring @Scheduled 默认使用单线程,任务阻塞会影响其他任务。 默认单线程就是所有任务挤一辆五菱宏光,一个任务堵路上,后面全迟到。
解决方案【优化】: 手动按阿里规范配置线程池,显式控制核心线程数、最大线程数、队列和拒绝策略,并加上简单监控埋点。
@Slf4j
@Configuration
@EnableScheduling
public class ScheduledConfig implements SchedulingConfigurer {
private static final int CORE_POOL_SIZE = 4;
private static final int MAX_POOL_SIZE = 8;
private static final int QUEUE_CAPACITY = 200;
private static final long KEEP_ALIVE_SECONDS = 60L;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("scheduled-worker-" + seq.getAndIncrement());
t.setDaemon(false);
return t;
}
},
// 拒绝策略:CallerRuns,防止任务静默丢失
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 【补充】线程池监控埋点示例:定期输出线程池指标
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
log.info("ScheduledPool metrics: poolSize={}, activeCount={}, queueSize={}",
executor.getPoolSize(), executor.getActiveCount(), executor.getQueue().size());
}, 60, 60, TimeUnit.SECONDS);
taskRegistrar.setScheduler(executor);
}
}
5.1.2 异常处理 —— 任务挂了别慌,先抓异常,再重试,还不行就喊人(告警)
规范要求【补充】: 定时任务必须捕获异常,避免任务停止;同时要考虑 Error 级异常(如 OOM)、失败重试与告警。
@Scheduled(cron = "0/30 * * * * ?")
public void scheduledTask() {
try {
doBusiness();
} catch (OutOfMemoryError oom) {
// Error 级异常兜底:记录关键信息并立即告警
log.error("定时任务 OOM,准备触发降级与告警", oom);
triggerAlarm("scheduledTask-oom");
fallback();
} catch (Throwable e) {
log.error("定时任务执行失败,准备失败重试与降级", e);
retryOrFallback(e);
}
}
private void retryOrFallback(Throwable e) {
// 【补充】简单失败重试 + 降级示例:
// 这里可以做一次重试,或将任务投递到 MQ/补偿表中异步处理
// retryOnce();
fallback();
triggerAlarm("scheduledTask-failed");
}
private void fallback() {
// 降级逻辑,比如只记录标记、延后处理、写本地文件等
}
private void triggerAlarm(String scene) {
// 告警逻辑,比如接入短信、邮件、钉钉/企业微信机器人等
log.warn("定时任务触发告警,scene={}", scene);
}
5.1.3 任务执行时间控制 —— 别固定睡 100ms,系统闲的时候多干点,忙的时候少歇点
问题: 任务执行时间过长可能导致任务重叠执行。
解决方案【补充】: 记录执行时长,并根据系统负载动态调整“休息时间”,而不是死板 Thread.sleep(100)。
@Scheduled(cron = "0/30 * * * * ?")
public void scheduledTask() {
long startTime = System.currentTimeMillis();
try {
doBusiness();
} finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > 30_000) {
log.warn("定时任务执行时间过长: {}ms", duration);
}
// 【补充】根据系统负载动态调整“节奏”,而不是死睡 100ms
double systemLoad = getSystemLoad(); // 比如来自 JMX / 操作系统指标
long delayMillis;
if (systemLoad < 0.3) {
// 系统很闲,多干点活
delayMillis = 10;
} else if (systemLoad < 0.7) {
// 正常负载,适当歇口气
delayMillis = 50;
} else {
// 系统很忙,别添乱
delayMillis = 200;
}
try {
Thread.sleep(delayMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private double getSystemLoad() {
// 示例:可以用 OperatingSystemMXBean 获取系统 load 或 CPU 使用率
return ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
}
5.1.4 SpringBoot 1.5.x vs 2.x 线程池配置差异
- 在 1.5.x 中,线程池多通过实现
SchedulingConfigurer、手动 new 线程池来配置,属于“手动挡”: 你得自己选排量(核心线程数)、档位(最大线程数)、油门(队列大小)、刹车(拒绝策略)。 - 在 2.x 中,官方提供了
spring.task.scheduling.*等自动配置入口,更像“自动挡”: 在配置文件里改改参数就能生效,底层还是线程池,只是变成了集中管理。 - 可以理解为:1.5 配线程池像开手动挡,爽是很爽,但得会“听发动机声音”;2.x 则更像自动挡,操作简单, 但无论手动还是自动,核心目标都是一个——让定时任务这条“道路”不卡、不堵。
5.2 集群环境下的分布式锁实践
5.2.1 Redis 分布式锁实现 —— 给集群任务上把锁,别让它们抢活干
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis 分布式锁工具类
* 【规范校验】:使用 Lua 脚本保证原子性,防止误删其他实例的锁
* 【版本兼容】:SpringBoot 1.5.x + Spring Data Redis 1.7.2
*/
@Component
public class DistributedLockUtil {
@Autowired
private StringRedisTemplate redisTemplate;
// 【补全】完整的 Lua 解锁脚本
// Lua 脚本是锁的“保镖”,保证拿锁、干活、放锁一气呵成,不会被其他线程打断。
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 尝试获取锁
* @param lockKey 锁的 key
* @param expireSeconds 过期时间(秒)
* @return 锁的 value(用于后续解锁);为空表示加锁失败
*/
public String tryLock(String lockKey, int expireSeconds) {
String lockValue = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result) ? lockValue : null;
}
/**
* 释放锁(使用 Lua 脚本保证原子性)
* @param lockKey 锁的 key
* @param lockValue 当前线程持有的锁 value
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String lockValue) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
lockValue
);
return result != null && result == 1;
}
}
5.2.2 使用示例
@Scheduled(cron = "0 0/10 * * * ?")
public void executeWithLock() {
String lockKey = "scheduled:task:sync";
String lockValue = UUID.randomUUID().toString();
int expireSeconds = 300; // 5 分钟
if (distributedLockUtil.tryLock(lockKey, lockValue, expireSeconds)) {
try {
log.info("获取分布式锁成功,开始执行任务");
doBusiness();
} finally {
distributedLockUtil.releaseLock(lockKey, lockValue);
log.info("释放分布式锁");
}
} else {
log.info("未获取到分布式锁,跳过本次执行");
}
}
5.3 任务监控与告警
5.3.1 任务执行监控
@Slf4j
@Component
public class MonitoredScheduledTask {
@Scheduled(cron = "0/30 * * * * ?")
public void monitoredTask() {
String taskName = "monitoredTask";
long startTime = System.currentTimeMillis();
boolean success = false;
try {
doBusiness();
success = true;
} catch (Exception e) {
log.error("定时任务执行失败: {}", taskName, e);
// 发送告警通知
sendAlert(taskName, e);
} finally {
long duration = System.currentTimeMillis() - startTime;
log.info("定时任务执行完成: task={}, success={}, duration={}ms",
taskName, success, duration);
// 记录监控指标
recordMetrics(taskName, success, duration);
}
}
private void sendAlert(String taskName, Exception e) {
// 实现告警逻辑(如发送邮件、短信、钉钉等)
}
private void recordMetrics(String taskName, boolean success, long duration) {
// 记录监控指标(如 Prometheus、InfluxDB 等)
}
}
5.4 性能优化建议
5.4.1 异步执行长时间任务
@Scheduled(cron = "0 0/10 * * * ?")
public void asyncTask() {
// 使用线程池异步执行,避免阻塞定时任务线程
executor.execute(() -> {
try {
doTimeConsumingBusiness();
} catch (Exception e) {
log.error("异步任务执行失败", e);
}
});
}
5.4.2 批量处理优化
@Scheduled(cron = "0 0/5 * * * ?")
public void batchProcessTask() {
int batchSize = 100;
int offset = 0;
while (true) {
List<Data> dataList = queryData(offset, batchSize);
if (dataList.isEmpty()) {
break;
}
// 批量处理
batchProcess(dataList);
offset += batchSize;
// 避免长时间占用线程,每批处理完休息一下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
六、总结
6.1 选择建议总结
- 简单单机任务(< 10 个):使用 Spring @Scheduled
- 集群环境简单任务:使用 Spring @Scheduled + 分布式锁
- 需要任务持久化:使用 Quartz(JDBC 模式)
- 需要动态管理任务:使用 Quartz 或 XXL-JOB
- 大规模分布式调度:使用 XXL-JOB 或 Elastic-JOB
6.2 关键注意事项
- 异常处理:定时任务必须捕获异常,避免任务停止
- 线程池配置:避免使用单线程,防止任务阻塞
- 集群环境:必须使用分布式锁或集群模式,避免重复执行
- 任务监控:记录任务执行时间、成功率等指标
- 性能优化:长时间任务使用异步执行,避免阻塞
6.3 SpringBoot 1.5.x 版本兼容性
| 方案 | 兼容性 | 注意事项 |
|---|---|---|
| Timer/TimerTask | ✅ 完全兼容 | 不推荐生产使用 |
| ScheduledExecutorService | ✅ 完全兼容 | JDK 1.5+ |
| Spring @Scheduled | ✅ 完全兼容 | 需 @EnableScheduling |
| Quartz | ✅ 兼容 | 推荐版本 2.1.7 |
| XXL-JOB | ✅ 兼容 | 需单独部署调度中心 |
附录:Cron 表达式参考
常用 Cron 表达式
| 表达式 | 说明 |
|---|---|
0 0 0 * * ? | 每天 0 点执行 |
0 0 12 * * ? | 每天 12 点执行 |
0 0/30 * * * ? | 每 30 分钟执行一次 |
0 0/10 * * * ? | 每 10 分钟执行一次 |
0 0 0/2 * * ? | 每 2 小时执行一次 |
0 0 0 ? * MON | 每周一 0 点执行 |
0 0 0 1 * ? | 每月 1 号 0 点执行 |
0 0 0 1 1 ? | 每年 1 月 1 号 0 点执行 |
Cron 表达式格式
秒 分 时 日 月 周 [年]
- 秒:0-59
- 分:0-59
- 时:0-23
- 日:1-31(注意月份天数)
- 月:1-12 或 JAN-DEC
- 周:1-7 或 SUN-SAT(1=周日)
- 年:可选,1970-2099
适用版本: SpringBoot 1.5.x、JDK 1.8+