Optimistic Locking Explained
Optimistic locking — also known as optimistic concurrency control — is a strategy that assumes conflicting updates are rare. Rather than stopping other writers from touching a record, it lets the work proceed and then verifies at write time that nothing changed underneath it — typically by checking a version number. If the version moved, the write is rejected and the caller retries. Pessimistic locking takes the opposite bet: acquire an exclusive lock before reading, hold it until the work commits, and make everyone else wait.
Both strategies solve the same problem — the lost update, a race condition in which two clients read the same value, modify it independently, and the second write silently erases the first. What separates them is when you pay the cost: optimistic locking pays on conflict, pessimistic locking pays on every operation.
How Optimistic Locking Works
The cycle has four steps. Read the record along with its current version. Compute the new value. Write it back conditionally, asserting that the version is still the one you read. If the conditional write affects no rows, another client got there first — so you either retry from the top or surface the conflict to the caller.
The version marker can be an integer counter, a last-modified timestamp, or the previous value itself in a compare-and-swap. In JPA and Hibernate, a @Version field gives you this for free; the persistence provider increments it on every update and throws OptimisticLockException when the check fails.
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version
private long version; // incremented automatically on every update
}
The part teams underestimate is the retry policy, which is part of the design rather than an afterthought. Retries should be bounded and ideally backed off; an unbounded retry loop under heavy contention degrades into a livelock, where every client burns CPU losing to every other client. It also helps if the retried operation is idempotent, so a retry that partially succeeded earlier does no additional damage.
How Pessimistic Locking Works
Pessimistic locking inverts the order: take the lock first, then read, modify and commit while holding it. A relational database does this with SELECT ... FOR UPDATE; a distributed system does it with a distributed lock. Because no other client can enter the critical section, a conflict is impossible by construction — there is nothing to detect and nothing to retry.
You pay for that certainty in throughput. Every operation carries lock acquisition overhead even when no one else wants the record, waiting clients block, and acquiring several locks in inconsistent order invites deadlock. In a distributed setting the risks are sharper still: a lock held by a process that crashes or stalls must expire on its own, which is why any serious distributed lock carries a lease and a renewal mechanism.
Optimistic vs Pessimistic Locking
| Optimistic locking | Pessimistic locking | |
|---|---|---|
| Conflict handling | Detected at write time | Prevented before the read |
| Cost under low contention | Almost none | Lock overhead on every operation |
| Cost under high contention | Repeated retries and wasted work | Queueing and waiting |
| Failure mode | Write rejected; caller retries | Blocking, lock timeouts, deadlock |
| Long-running work | Risky — a wide window to be invalidated | Safe, but blocks everyone else |
| Deadlock risk | None | Real, and grows with lock count |
| Best for | Read-heavy, low-conflict workloads | Write-heavy or costly-to-retry work |
Neither is universally correct, and the choice is empirical rather than stylistic. Two questions settle it: how often do two clients actually contend for the same record, and how expensive is it to redo the work? Low contention with cheap retries favours optimistic locking. Frequent contention, or work that is slow, side-effecting, or awkward to repeat, favours pessimistic locking. Most systems end up using both — optimistic for ordinary record updates, pessimistic around the few operations that genuinely cannot be replayed.
Optimistic Locking in Valkey and Redis
Valkey and Redis support optimistic locking natively through WATCH. Marking one or more keys with WATCH before opening a transaction tells the server to abort at EXEC if any of those keys changed in the meantime — a version check by another name.
Redisson, the Valkey and Redis Java client, exposes the same idea more directly. RBucket and RAtomicLong both provide compareAndSet(), which swaps a value only if it still matches what you read — no transaction required. The bounded retry loop below is the shape most production code takes:
RBucket<Inventory> bucket = redisson.getBucket("inventory:42");
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
Inventory current = bucket.get();
if (current.getStock() == 0) {
throw new OutOfStockException();
}
Inventory updated = current.withStock(current.getStock() - 1);
// succeeds only if nobody changed the value in the meantime
if (bucket.compareAndSet(current, updated)) {
return updated;
}
// another client won the race — re-read and try again
}
throw new ConcurrentUpdateException();
Unlike setIfAbsent() or getAndSet(), compareAndSet() has no single-command equivalent in Valkey or Redis. Redisson implements it with a server-side script, so the comparison and the write still happen as one indivisible step. One consequence is worth knowing: the comparison runs against the serialized value, so your codec must encode equal objects to identical bytes. A field whose encoding varies between runs — an unordered collection, or a timestamp captured at write time — will make the swap fail even when nothing actually changed.
When the conflict spans several objects rather than one key, RTransaction groups the work into an atomic unit with real rollback — see How to Manage Transactions in Valkey and Redis on Java for a walkthrough.
Before reaching for any of it, check whether the operation is already atomic. A plain counter needs no version check at all, because INCR and Redisson's decrementAndGet() are indivisible on the server, and a claim staked with SETNX is too. Optimistic locking earns its place when the new value depends on more of the record than one command can express — which is exactly the compound object above. The Redis SET command in Java covers the full family of conditional writes.
Pessimistic Locking in Valkey and Redis
Where the work genuinely cannot be retried, Redisson's RLock implements java.util.concurrent.locks.Lock across processes and machines, so a critical section holds cluster-wide rather than within a single JVM:
RLock lock = redisson.getLock("inventory:42");
lock.lock();
try {
// exclusive across every application instance
} finally {
lock.unlock();
}
Two details make this safe in production. A watchdog renews the lease while the holder is still working, so a slow task does not lose its lock mid-flight. And a replica-synchronization check, enabled by default, withholds the lock until the write has reached a replica — closing the failover gap in which a lock is granted, the primary dies before replicating, and a promoted replica hands the same lock to somebody else.
For the strictest cases, RFencedLock issues a monotonically increasing fencing token via lockAndGetToken(). The protected resource records the highest token it has seen and rejects anything lower, so a stalled holder that wakes up late cannot write over a newer one. This pairing of replica synchronization and fencing is why Redisson has deprecated its older RedLock implementation. Practical guidance lives in How to Use Redis Locks in Java, and the wider family of Java locks covers read-write locks, semaphores and latches built on the same foundation.
Optimistic Locking: Frequently Asked Questions
What Is the Difference Between Optimistic and Pessimistic Locking?
Optimistic locking allows concurrent work and detects conflicts when the write is attempted, usually with a version check, rejecting and retrying the loser. Pessimistic locking prevents conflicts by taking an exclusive lock before the read and holding it until commit. Optimistic pays only when a conflict actually happens; pessimistic pays on every operation.
When Should You Use Optimistic Locking?
Use it when contention for the same record is low and redoing the work is cheap — a profile that fits most read-heavy applications. Prefer pessimistic locking when writes to the same record are frequent, when the operation is slow or has side effects, or when a retry would be visible to the user.
Is Optimistic Locking Really a Lock?
Not in the usual sense. Nothing is ever held and no client is ever blocked. The name describes the outcome rather than the mechanism: a conditional write that fails when someone else has intervened. Optimistic concurrency control is the more precise term, and the two are used interchangeably.
Do Valkey and Redis Support Optimistic Locking?
Yes. The WATCH command marks keys so a transaction aborts if any of them change before EXEC. Through Redisson, RAtomicLong and RBucket also expose compareAndSet() for single-key compare-and-swap, and RTransaction covers cases that span several objects.
What Happens When an Optimistic Lock Fails?
The write is rejected and no data changes. Your code then either retries the whole read-modify-write cycle against the current value, or reports the conflict to the caller. JPA signals this with OptimisticLockException. Retries should be bounded and backed off, since unbounded retrying under contention wastes capacity without making progress.