用 Spring 自带的 @Scheduled 做定时任务,本地一般都正常。一上多实例就不一样了。同一个任务,两台机器各跑一遍,消息发重,数据也对不上。翻日志翻半天,才想起来:@Scheduled 没有集群这回事。每个实例自己触发自己的,旁边那台它看不见。
很多人下一上去就是 Quartz。打开集群模式文档,先建表:QRTZ_JOB_DETAILS、QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS、QRTZ_FIRED_TRIGGERS,数一下十一张,配置也不少。项目里本来就两个定时任务,铺十一张表,我觉得过了。
db-scheduler 只要一张表,能在集群里跑,嵌进现有 Spring Boot 就行。
db-scheduler 是什么
Gustav Karlsson 写的 Java 调度库,持久化的。他自己的说法:受 ScheduledExecutorService 启发,比 Quartz 简单,集群里也能用。
多个实例同时跑,某次执行只会被其中一个抢到。互斥走数据库乐观锁,或者 select for update。ZooKeeper、Redis 都不用接。调度信息和执行状态在库里,进程重启不会丢,机器挂了也不会丢。
元数据全在一张 scheduled_tasks 里。它是个库,跑在你应用进程里。官方压测大约每秒 2000 到 10000 次执行(Postgres,4 个实例)。核心库只依赖 slf4j。
两种任务。周期的:每小时一次,每天 3 点一次。一次性的:5 分钟后跑,或者业务事件之后延迟执行。Spring Task 只管周期。一次性延迟,以前要么上消息队列,要么自己拿 ScheduledExecutorService 写。
跟 Spring Task、Quartz 比,差在哪
| 维度 | Spring Task(@Scheduled) |
Quartz | db-scheduler |
|---|---|---|---|
| 持久化 | 无,重启即丢 | 有 | 有 |
| 集群互斥 | 无,需要额外方案 | 支持,但配置较重 | 原生支持 |
| 所需数据表 | 0 | 11 张(QRTZ_*) |
1 张 |
| 一次性延迟任务 | 不支持 | 支持 | 支持 |
| 部署形态 | 嵌入应用 | 嵌入应用 | 嵌入应用 |
| 上手成本 | 最低 | 较高 | 低 |
@Scheduled 单实例时够用。上了集群,锁得自己想办法。
ShedLock 是给 @Scheduled 配的分布式锁。任务还是 Spring Task 跑,锁另外管。只是不想让定时任务在集群里跑重,它够轻。任务持久化和一次性延迟,它不管。
Quartz 触发器、日历排除、任务分组、失败重试,该有的都有。Java 生态里当了很多年标准答案。代价是十一张表,以及 JobDetail / Trigger / Scheduler 三层概念。JobStore 对事务隔离级别还有要求。db-scheduler 的 FAQ 写过,它一开始就是给 schema 不大的应用用的。中小项目为两个 cron 塞十一张表,不划算。
要报警、多团队共用一套任务平台,去看 XXL-JOB。那是调度平台,得单独部署调度中心。项目还在扁平快阶段,这笔账算不过来。只是想打开页面看任务跑没跑,不必为此再养一套。
为什么适合「扁平快」的项目
扁平快,我指的是栈不想铺、部署链路短、人不多、发版勤。定时任务这种需求,不想再养一个中间件。
db-scheduler 跟着 Spring Boot 一起启停,没有独立进程要盯,也不用单独申请容器。一张表,字段不到十五个。任务写成 Spring Bean,注册和抢锁交给 starter。
已经在用 Spring Boot 的中小型后端,接入成本不高。集群不会跑重,也能按订单、按用户去排一次性延迟任务。
快速上手
下面按 Spring Boot 4.x 写。3.x 把 starter 的 artifactId 换成 db-scheduler-spring-boot-starter,其余一样。db-scheduler 从 16.x 起要求 Java 17+,先确认项目的 JDK。
引入依赖
1 | <dependency> |
建表
官方仓库给主流数据库都备了 DDL,这里用 MySQL:
1 | create table scheduled_tasks ( |
MySQL 和 MariaDB 用的是不带时区的 TIMESTAMP / DATETIME。官方要求这两种库必须开 .alwaysPersistTimestampInUTC(),跨时区部署时执行时间会偏。Spring Boot starter 的默认属性没暴露这个开关,自己注册一个 DbSchedulerCustomizer Bean。几行代码,官方仓库有示例。
基础配置
1 | db-scheduler: |
polling-interval 是去数据库扫到期任务的间隔,默认 10 秒。调度精度的下限就是这个数。它不是内存里的定时器,先求可靠,不求毫秒级准。真要任务一到点就跑,打开 immediate-execution-enabled。schedule() 之后调度器会提前看一眼,不用干等到下一个轮询周期。
写一个周期任务
声明成 RecurringTask 类型的 Bean,starter 启动时自己注册:
1 |
|
FixedDelay.ofMinutes(5) 是上次跑完再隔 5 分钟。要每天固定点,换成 Schedules.cron("0 0 3 * * ?") 或 Schedules.daily(LocalTime.of(3, 0))。
写一个一次性任务
先定义执行逻辑,再用的时候排进去。
1 | public static final TaskDescriptor<OrderReminderData> ORDER_REMINDER_TASK = |
业务代码里触发调度:
1 |
|
SchedulerClient 是 starter 装配好的 Bean,注入就能用,不用自己构建 Scheduler。
下单后 30 分钟检查支付,没付就提醒。@Scheduled 很难写成「每个订单各自延迟 30 分钟」。上延迟消息又要多一个中间件。一次性任务正好干这个。
想看界面:db-scheduler-ui

db-scheduler 自己没有控制台。官方 README 把 db-scheduler-ui 列在第三方扩展里,Bekk 做的,嵌在你的 Spring Boot 应用里。
页面上能看到 Scheduled、Running、Failed。失败了可以点重跑,也能删任务。
Spring Boot 4 加这个 starter。
1 | <dependency> |
5.x 起日志写入已经打进 UI starter,不用再单独引 db-scheduler-log。
生产环境把 /db-scheduler 和 /db-scheduler-api 用 Spring Security 挡住。只给看、不给点,开 db-scheduler-ui.read-only=true。
多团队和独立报警后台,那是调度平台的事。这个 UI 只管看自己应用里的任务。