跪拜 Guibai
← All articles
Frontend · Backend

Optimistic vs. Pessimistic Locking: When to Use Each and How to Wire Them End-to-End

By 袋鱼不重 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Lost updates and oversells are two of the most common data-corruption bugs in multi-user web apps. Choosing the wrong lock strategy either burns throughput with unnecessary waits or silently drops writes that should have succeeded.

Summary

Two users editing the same order can silently overwrite each other's changes. Optimistic locking solves this by adding a version column that increments on every update; the write only succeeds if the version matches what was originally read. The version number must travel to the frontend and back on every write — it acts as a concurrency credential, not a secret. Pessimistic locking takes the opposite approach: it locks the row at read time so no one else can touch it until the transaction completes.

Inventory deductions, flash sales, and payment callbacks are poor fits for optimistic locking because conflict rates are high and failure is unacceptable. Atomic SQL (`UPDATE stock SET qty = qty - n WHERE qty >= n`) handles inventory without explicit locks. Row-level `SELECT ... FOR UPDATE` works for short-lived state transitions like shipment confirmation. Redis distributed locks serialize cross-service jobs such as settlement generation.

A practical checklist covers both sides: backends must return clear conflict errors instead of silent failures; frontends must treat the version as part of the payload and refresh on conflict rather than blindly retrying. Snowflake IDs stay as strings to avoid JavaScript precision loss, while version numbers remain integers since they never approach `Number.MAX_SAFE_INTEGER`.

Takeaways
Optimistic locking uses a version column incremented on each successful update; the write fails if the version no longer matches what was read.
Pessimistic locking serializes access by locking the row at read time, blocking other writers until the transaction ends.
Atomic SQL (`UPDATE ... WHERE qty >= n`) is the highest-performance way to prevent overselling without explicit locks.
Redis distributed locks are the right tool when mutual exclusion must span multiple services or nodes.
Frontends must carry the version number on every write and refresh data on conflict; retrying with a stale version guarantees repeated failures.
Snowflake IDs exceed JavaScript's safe integer range and must be transmitted as strings; version numbers are small enough to stay as numbers.
Backends must return an explicit conflict error when `affected_rows = 0` — silent success is a data-loss bug.
Long-held locks (user editing a form for 30 minutes) are a classic anti-pattern for pessimistic locking; optimistic locking is the correct fit.
Conclusions

Exposing the version number to the frontend is not a security concern — it is a stateless concurrency token, and the industry standard across JPA, MyBatis, and REST APIs.

Many teams overuse optimistic locking for inventory and underuse it for documents. The decision tree is straightforward: if the user holds state for minutes, use optimistic; if the operation is a sub-second API call that must succeed, use pessimistic or atomic.

The checklist format for frontend, backend, and product roles is a practical artifact that teams can adapt directly into their definition-of-done for any feature touching shared mutable state.

Combining strategies is normal: a single system might use optimistic locking on the order header, atomic SQL on inventory, and a Redis lock on settlement generation.

Concepts & terms
Lost Update
A concurrency bug where two transactions read the same data, and the later write silently overwrites the earlier one because neither saw the other's changes.
Optimistic Locking
A concurrency control strategy that assumes conflicts are rare. It checks at commit time whether the data has changed since it was read, typically using a version number or timestamp.
Pessimistic Locking
A concurrency control strategy that assumes conflicts are likely. It locks the data at read time so no other transaction can modify it until the lock is released.
Atomic SQL
A single UPDATE statement that includes the safety condition in the WHERE clause (e.g., `WHERE qty >= n`), performing the read-check-write in one database operation without explicit locks.
Snowflake ID
A distributed unique ID generation algorithm producing 64-bit integers. The resulting 15–19 digit numbers exceed JavaScript's `Number.MAX_SAFE_INTEGER` (2^53-1), so they must be handled as strings in frontend code.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗