Add the scheduling module

Three replicas of one application against one PostgreSQL database, proving
duplicate @Scheduled execution and then removing it with ShedLock: 24
executions where 8 were due, then 7 for 7 ticks.

Also measured: @SchedulerLock without @EnableSchedulerLock does nothing and
warns about nothing; spring.task.scheduling.pool.size=1 does not starve a
fixedRate job but delays it and fires 35 of 40 executions in a burst; and a
node whose clock is 40 seconds fast takes a live lock unless the provider
uses usingDbTime().
This commit is contained in:
2026-09-01 23:28:06 +05:30
parent 243cccd4ca
commit 6af6e4a3f1
31 changed files with 1373 additions and 1 deletions

View File

@@ -0,0 +1,46 @@
prev: [Clock skew](04-clock-skew.md) · [README](../README.md)
# 5. When not to use a lock
ShedLock is small, it has one dependency on your database, and it solves the stated problem. It
is still worth asking whether the problem should exist.
**Make the job idempotent instead.** A lock is a way of avoiding a second execution. Idempotence
is a way of not caring about one. If the job's work can be keyed — "mark invoices dated
2026-09-01 as sent, where they are not already marked" — then three replicas running it produce
one outcome and you have removed a distributed-systems dependency rather than adding one. This is
almost always the better engineering, and it is almost never what the article you searched for
suggests.
**A lock is not a guarantee of exactly-once.** It is a guarantee of at-most-one-per-lock-window,
which is different. If the holder dies halfway, the job did not complete and nothing retries it —
ShedLock has no notion of failure, only of expiry. Pairing it with a job table that records
completion is what makes "exactly once" true, and at that point the job table is doing most of
the work.
**Consider the scheduler you already have.** A Kubernetes `CronJob` runs one pod per schedule and
needs no lock, no library and no table; it costs you a pod start per run and the schedule lives
outside the application. Quartz in clustered mode owns misfire policy, persistence and recovery,
at the price of eleven tables and a great deal of configuration. ShedLock sits between them: it
adds a lock to the scheduler you already have, and deliberately adds nothing else.
**A lock provider needs its store to be consistent.** The JDBC provider is safe because a
conditional `UPDATE` on one row is atomic in every relational database. The Redis provider is
safe on a single instance and, as its own documentation notes, is subject to the well-known
argument about locks over Redis replication. If your store is eventually consistent, your lock
is too.
## A short checklist for the version you ship
- `@EnableSchedulerLock` present, and a `LockProvider` bean present — without both, the
annotation does nothing ([chapter 1](01-three-replicas-three-executions.md))
- `usingDbTime()` on ([chapter 4](04-clock-skew.md))
- `lockAtMostFor` longer than the worst-case run time, and you have accepted the gap it implies
after a crash
- `lockAtLeastFor` non-zero for any job shorter than the interval between two replicas' timers
- `spring.task.scheduling.pool.size` raised above 1 ([chapter 3](03-one-scheduler-thread.md))
- the lock names are unique per job — two jobs sharing a name share a lock, and one of them stops
running
- the method carrying `@SchedulerLock` is called from outside its own class, and is not `final`
[README](../README.md)