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().
47 lines
2.7 KiB
Markdown
47 lines
2.7 KiB
Markdown
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)
|