Two Scheduled Tasks, Eleven Quartz Tables? db-scheduler Offers a Single-Table Alternative for Spring Boot
The article compares Spring @Scheduled, Quartz, and db-scheduler for clustered scheduling, showing how db-scheduler uses a single database table to provide cluster-safe mutex, persistence, and one-time delayed tasks without the overhead of Quartz's eleven tables or external middleware.
Using Spring's built-in @Scheduled works fine locally, but in a multi-instance cluster the same task runs on every node, causing duplicate messages and data inconsistency. @Scheduled has no cluster awareness; each instance triggers independently.
Many teams reach for Quartz next. Enabling its cluster mode requires creating eleven tables ( QRTZ_JOB_DETAILS, QRTZ_TRIGGERS, QRTZ_CRON_TRIGGERS, QRTZ_FIRED_TRIGGERS, etc.) and configuring the three-layer JobDetail / Trigger / Scheduler model plus JobStore transaction isolation. For a project with only two scheduled jobs, that schema cost is disproportionate.
What db-scheduler Is
Written by Gustav Karlsson, db-scheduler is a persistent Java scheduling library inspired by ScheduledExecutorService but simpler than Quartz and cluster-capable. Multiple instances compete for each execution; only one wins via database optimistic locking or select for update — no ZooKeeper or Redis required. Scheduling metadata and execution state live in the database, surviving process restarts and machine failures.
All metadata fits in one scheduled_tasks table (fewer than 15 columns). The core library depends only on slf4j. Official benchmarks show roughly 2,000–10,000 executions per second on PostgreSQL with four instances.
It supports two task types: recurring (e.g., every hour, daily at 3 AM) and one-time (run in 5 minutes, or after a business event). Spring Task only handles recurring; one-time delays previously required a message queue or manual ScheduledExecutorService code.
Comparison: Spring Task vs Quartz vs db-scheduler
Persistence : Spring Task — none, lost on restart; Quartz — yes; db-scheduler — yes
Cluster mutex : Spring Task — none, needs extra solution; Quartz — supported, but heavy config; db-scheduler — native support
Tables required : Spring Task — 0; Quartz — 11 ( QRTZ_*); db-scheduler — 1
One-time delayed tasks : Spring Task — not supported; Quartz — supported; db-scheduler — supported
Deployment : All three — embedded
Learning curve : Spring Task — lowest; Quartz — high; db-scheduler — low @Scheduled suffices for single instances; in a cluster you must add your own locking. ShedLock adds a distributed lock to @Scheduled but does not provide persistence or one-time tasks.
Quartz offers triggers, calendar exclusions, job grouping, retry — the long-time Java standard — but at the cost of eleven tables and the JobDetail / Trigger / Scheduler abstraction layers. Its FAQ notes it targets larger schemas; for small projects with a couple of cron jobs, the overhead is not worthwhile.
If you need alerting, multi-tenant task platforms, or a separate admin console, look at XXL-JOB. That is a full scheduling platform requiring a dedicated scheduler center. For a flat, fast-moving codebase that just wants to see whether tasks ran, running another system is unjustified.
Why It Fits "Flat-Fast" Projects
"Flat-fast" means minimal stack, short deployment chains, small teams, frequent releases. You don't want to operate another middleware. db-scheduler starts and stops with Spring Boot, no separate process to monitor, no extra container. One table, tasks written as Spring Beans, registration and locking handled by the starter. Existing Spring Boot backends can adopt it with low friction: cluster-safe execution plus per-order or per-user one-time delayed tasks.
Quick Start (Spring Boot 4.x)
Add Dependency
<dependency>
<groupId>com.github.kagkarlsson</groupId>
<artifactId>db-scheduler-spring-boot-4-starter</artifactId>
<version>16.12.0</version>
</dependency>For Spring Boot 3.x use artifact db-scheduler-spring-boot-starter. db-scheduler 16.x requires Java 17+.
Create Table (MySQL)
create table scheduled_tasks (
task_name varchar(100) not null,
task_instance varchar(100) not null,
task_data blob,
execution_time datetime(6) not null,
picked BOOLEAN not null,
picked_by varchar(50),
last_success datetime(6) null,
last_failure datetime(6) null,
consecutive_failures INT,
last_heartbeat datetime(6) null,
version BIGINT not null,
priority SMALLINT,
PRIMARY KEY (task_name, task_instance),
INDEX execution_time_idx (execution_time),
INDEX last_heartbeat_idx (last_heartbeat),
INDEX priority_execution_time_idx (priority desc, execution_time asc)
);MySQL and MariaDB use non-timezone TIMESTAMP / DATETIME. The library requires .alwaysPersistTimestampInUTC() for cross-timezone deployments. The Spring Boot starter doesn't expose this switch; register a DbSchedulerCustomizer bean (example in the official repo).
Basic Configuration
db-scheduler:
enabled: true
table-name: scheduled_tasks
polling-interval: 10s
threads: 10
heartbeat-interval: 5m
immediate-execution-enabled: false polling-intervalis the database scan interval for due tasks (default 10 s); it sets the lower bound on scheduling precision. The library favors reliability over millisecond accuracy. Enable immediate-execution-enabled to have the scheduler check immediately after schedule() instead of waiting for the next poll.
Recurring Task
Declare a RecurringTask bean; the starter registers it on startup:
@Configuration
public class ScheduledTasksConfig {
@Bean
public RecurringTask<Void> syncOrderStatusTask() {
return Tasks.recurring("sync-order-status", FixedDelay.ofMinutes(5))
.execute((instance, ctx) -> {
// business logic, e.g., sync order status
log.info("同步订单状态任务执行");
});
}
} FixedDelay.ofMinutes(5)means 5 minutes after the previous run finishes. For fixed wall-clock times use Schedules.cron("0 0 3 * * ?") or Schedules.daily(LocalTime.of(3, 0)).
One-Time Task
Define the execution logic first, then enqueue when needed:
public static final TaskDescriptor<OrderReminderData> ORDER_REMINDER_TASK =
TaskDescriptor.of("order-reminder", OrderReminderData.class);
@Bean
public OneTimeTask<OrderReminderData> orderReminderTask() {
return Tasks.oneTime(ORDER_REMINDER_TASK)
.execute((instance, ctx) -> {
OrderReminderData data = instance.getData();
log.info("发送订单提醒,orderId={}", data.orderId);
});
}Trigger from business code:
@Service
@RequiredArgsConstructor
public class OrderService {
private final SchedulerClient schedulerClient;
public void createOrder(Long orderId) {
// ... create order logic
// 30 minutes later check payment, remind if unpaid
schedulerClient.schedule(
ScheduledTasksConfig.ORDER_REMINDER_TASK
.instance(String.valueOf(orderId))
.data(new OrderReminderData(orderId))
.scheduledTo(Instant.now().plusSeconds(1800))
);
}
} SchedulerClientis a starter-provided bean; inject and use directly. @Scheduled cannot express "each order delays 30 minutes independently"; adding a message queue would introduce another middleware. One-time tasks solve this exactly.
Optional UI: db-scheduler-ui
db-scheduler has no built-in console. The official README lists db-scheduler-ui (by Bekk) as a third-party extension that embeds in your Spring Boot app.
The UI shows Scheduled, Running, Failed tasks; failed tasks can be re-run or deleted.
Add for Spring Boot 4:
<dependency>
<groupId>no.bekk.db-scheduler-ui</groupId>
<artifactId>db-scheduler-ui-spring-boot-4-starter</artifactId>
<version>5.0.0</version>
</dependency>
db-scheduler-ui:
history: true
log:
enabled: trueSince 5.x logging is bundled in the UI starter; no separate db-scheduler-log needed. In production, protect /db-scheduler and /db-scheduler-api with Spring Security. For read-only access set db-scheduler-ui.read-only=true. This UI only covers tasks inside your own application; multi-team platforms and independent alerting remain the domain of full scheduling platforms.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
