Files
spring-async-demo/scheduling/docs/05-when-not-to-use-a-lock.md
Ankur Mhatre eaa8171966 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().
2026-09-01 23:32:16 +05:30

2.7 KiB

prev: Clock skew · README

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)
  • usingDbTime() on (chapter 4)
  • 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)
  • 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