几乎每个后端服务都离不开 Java 定时任务:每天凌晨跑批、定时清理过期订单、整点同步第三方数据。但很多团队一开始用 @Scheduled 随手写,业务一上量就遇到重复执行、任务堆积、实例宕机漏跑等问题。本文从单机到分布式,对比 Spring @Scheduled、Quartz 与 XXL-JOB 三种方案,帮你选对工具、避开生产坑。
一、Spring @Scheduled:单机最快落地
如果服务只有单实例、任务不复杂,Spring 自带的 @Scheduled 是最省心的选择。加一个 @EnableScheduling,再在方法上标注调度规则即可,零额外依赖。它支持三种触发方式:固定速率(fixedRate)、固定延迟(fixedDelay)和 cron 表达式。
@Configuration
@EnableScheduling
public class ScheduleConfig {}
@Component
public class OrderJob {
// 固定速率:以上次“开始时间”为基准,每 5 秒触发
@Scheduled(fixedRate = 5000)
public void syncEvery5s() {
orderService.sync();
}
// 固定延迟:以上次“结束时间”为基准,结束后再等 5 秒
@Scheduled(fixedDelay = 5000)
public void syncAfterFinish() {
orderService.cleanExpired();
}
// cron:每天 02:30 跑批(秒 分 时 日 月 周)
@Scheduled(cron = "0 30 2 * * ?")
public void dailyBatch() {
reportService.daily();
}
}
三种方式的核心差异在于“计时起点”不同,直接决定任务会不会堆叠。下面这张对照表是选型的第一道分水岭。
| 方式 | 计时基准 | 适合场景 | 主要风险 |
| fixedRate | 上次开始时间 | 准点采集、心跳上报 | 任务耗时>间隔会堆叠 |
| fixedDelay | 上次结束时间 | 串行 cleanup、导出 | 耗时越长越滞后 |
| cron | 绝对时间点 | 日/月批处理 | 时区、宕机漏跑 |
1.1 单机最大的坑:默认单线程
@Scheduled 默认使用单线程的 TaskScheduler,一个任务卡住,其它任务全部排队。生产环境一定要自定义线程池,或者把耗时逻辑丢到 Spring Boot 异步线程池 @Async 执行,避免阻塞调度线程。任务真正并发执行时,线程池的参数调优可参考 Java 线程池调优:ThreadPoolExecutor 参数配置与监控告警。
二、Quartz:需要持久化与集群时
当任务需要“重启不丢、中途可暂停、多实例不重复”时,@Scheduled 就不够了。Quartz 通过 JDBC JobStore 把 Job 和 Trigger 存进数据库,天然支持集群——多台机器抢同一把数据库锁来避免重复触发,还能动态增删改任务。
public class CleanJob implements Job {
@Override
public void execute(JobExecutionContext ctx) {
// 业务逻辑:清理历史日志
logService.purge(30);
}
}
// 调度端配置
JobDetail job = JobBuilder.newJob(CleanJob.class)
.withIdentity("cleanLog", "group1")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("tClean", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 3 * * ?"))
.build();
scheduler.scheduleJob(job, trigger);
Quartz 的 Job 与 Trigger 解耦设计,让“同一份逻辑、不同周期”可以复用。和 @Scheduled 的选型对照如下。
| 维度 | @Scheduled | Quartz |
| 任务持久化 | 否(内存) | 是(JDBC 存储) |
| 集群防重复 | 不支持 | 支持(DB 锁) |
| 动态增删任务 | 需改代码重启 | 运行时 API 操作 |
| 学习成本 | 极低 | 中等 |
三、XXL-JOB:多实例分布式调度
当服务横向扩成多个副本(典型如 Spring Cloud 微服务 架构),再靠 @Scheduled 或 Quartz 单机集群就会很别扭:要么重复跑,要么运维复杂。XXL-JOB 把“调度中心”和“执行器”分离,由调度中心统一触发、执行器只负责干活,天然适配分布式,还自带可视化、失败重试、告警和分片广播。
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello");
// 分片参数:把大任务拆给多个执行器并行处理
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
List<Long> ids = orderMapper.selectByShard(shardIndex, shardTotal);
for (Long id : ids) {
orderService.settle(id);
}
}
分片广播是 XXL-JOB 的杀手锏:一次触发,所有执行器各自处理属于自己的那一片数据,既能水平扩容又能避免重复。配合失败重试和调度日志,基本覆盖了中大型团队的调度诉求。
XXL-JOB 还内置多种路由策略应对不同负载:第一个/最后一个、轮询、一致性哈希、分片广播等。对“全量扫描后再分发”的场景,分片广播最合适;对“只跑一次即可”的场景,用单节点路由避免重复。配合调度中心的失败重试次数与告警邮箱,任务异常能在分钟级触达负责人,而不是等第二天对账才发现。
四、三大方案选型与避坑清单
把三种方案放在一起看,其实是一条清晰的演进路线:从“进程内写个注解”到“独立调度框架管生命周期”,再到“中心化调度平台管多实例”。选型时不要盲目追新,先问清楚自己的服务是单实例还是多副本、任务要不要动态管理、团队有没有精力运维中间件,答案往往就出来了。
| 方案 | 最适合 | 不适合 |
| @Scheduled | 单机小任务、原型验证 | 多实例、需动态管理 |
| Quartz | 需持久化/集群的中型调度 | 超大规模、可视化弱 |
| XXL-JOB | 分布式、多团队、需监控告警 | 不愿引入额外中间件 |
4.1 生产环境五大避坑
① 任务重叠:fixedRate 任务耗时超过间隔会堆叠,长任务改用 fixedDelay 或异步执行。
② 多实例重复:单机 @Scheduled 在多副本下必然重复,必须上 Quartz 集群或 XXL-JOB。
③ 时区陷阱:cron 默认取服务器时区,容器常用 UTC,会导致“半夜跑白天”。务必显式指定 zone 或用北京时间基线。
④ 事务边界:定时任务里写库要留意 Spring 事务传播机制,自调用会导致 @Transactional 失效。
⑤ 幂等兜底:任务可能被重试或重复触发,业务逻辑必须幂等(状态机+唯一约束),否则重复跑批会酿成资损。
⑤ 监控不可少:再稳的调度也会翻车。把每次执行的开始/结束/耗时写入日志或埋点,配合告警,才能在第一时间发现“今天批没跑”。
幂等的具体落地通常有两招:一是利用数据库唯一约束,把“任务实例 id + 业务日期”做成唯一键,重复插入直接被忽略;二是用状态机,先查询当前状态再决定动作,配合版本号或乐观锁。凡是涉及金额、库存、积分的批处理,宁可多花一倍代码把幂等做扎实,也别去赌“它只会跑一次”。
五、总结
选 Java 定时任务 方案时遵循“够用就好”:单实例轻量需求用 @Scheduled,要持久化与集群用 Quartz,多实例分布式统一用 XXL-JOB。无论选哪个,线程池隔离、时区显式、幂等兜底、执行监控这四条都是上线前的必检项。把调度当成一个有状态的组件来对待,才能让半夜的批处理真正“睡得安稳”。




