Optimistic vs. Pessimistic Locking: When to Use Each and How to Wire Them End-to-End
This article explains the core principles, selection criteria, and front-end/back-end collaboration methods for optimistic locking and pessimistic locking, with practical examples. Suitable for front-end and back-end development, architecture design, and technical sharing.
1. What Problem Are We Solving?
1.1 Concurrent Write Conflict: Lost Update
When two users modify the same data almost simultaneously, the later save might silently overwrite the earlier save, causing data loss.
Typical scenario:
Timeline:
─────────────────────────────────────────────────────
T1 User A opens "Order A", reads version = 1, receiver = Zhang San
T2 User B opens the same order, also reads version = 1, receiver = Zhang San
T3 User B changes receiver to "Li Si" and saves → Success, DB version = 2
T4 User A changes receiver to "Wang Wu" and saves, still submits with version = 1
→ Without optimistic lock: Wang Wu overwrites Li Si (B's modification is lost)
→ With optimistic lock: Update fails, prompts refresh (A must re-read version = 2 before operating)
This is the classic Lost Update problem. The goal of optimistic locking is not to "prevent multiple people from viewing simultaneously," but to prevent an old snapshot from overwriting new data.
1.2 Optimistic Locking vs. Pessimistic Locking
| Dimension | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| Basic Assumption | Low conflict probability, read-heavy, write-light | High conflict probability, write-intensive |
| Lock Timing | Checks for conflict at commit time | Locks the record at read time |
| Implementation | version field / timestamp |
SELECT ... FOR UPDATE, distributed lock |
| Concurrency Performance | High, read operations are non-blocking | Low, others must wait while lock is held |
| Conflict Handling | Update fails, business layer prompts retry | Wait for lock release or timeout |
| Typical Scenarios | Business document editing (purchase plans, orders) | Inventory deduction, flash sales, account balances |
Selection Rule of Thumb:
- Documents, forms, infrequent conflicts → Optimistic Locking
- Hotspot counters, funds, inventory → Pessimistic Locking or Atomic SQL (
UPDATE qty = qty - 1 WHERE qty >= 1)
2. Core Mechanism of Optimistic Locking
2.1 The version Field
Add a version number field to the business table. On every successful update, version = version + 1.
-- The update must carry the "version number at the time of reading"
UPDATE biz_order
SET receiver_name = ?,
update_time = NOW(),
version = version + 1
WHERE id = ?
AND version = ?; -- Old version passed from front-end/caller
| Update Result | Meaning |
|---|---|
affected_rows = 1 |
No one modified it first, update successful, version auto-increments by 1 |
affected_rows = 0 |
Record was modified by someone else, this write is rejected |
2.2 Complete Interaction Flow
sequenceDiagram
participant Frontend
participant Backend
participant DB
Frontend->>Backend: Query plan (list/detail)
Backend->>DB: SELECT id, ..., version
DB-->>Frontend: Return data + version = 1
Note over Frontend: User edits (may stay for a long time)
Frontend->>Backend: Submit modification { id, version: 1, ... }
Backend->>DB: UPDATE ... WHERE id=? AND version=1
alt Still version=1 (no one changed it)
DB-->>Backend: Update 1 row, version → 2
Backend-->>Frontend: Success
else Already version=2 (someone changed it)
DB-->>Backend: Update 0 rows
Backend-->>Frontend: Version conflict, operation failed
Frontend-->>Frontend: Prompt "Data has been modified, please refresh and retry"
end
2.3 Why Expose version to the Frontend?
The essence of optimistic locking is: The caller declares, "This modification I'm making is based on the data I originally saw."
The backend cannot know which version the user read when opening the page, so the write request must carry:
{
"id": "1234567890123456789",
"version": 1,
"receiverName": "9876543210987654321"
}
"I believe the current version is
1, please make the change under this premise."
Why Not Let the Backend Remember It?
| Approach | Problem |
|---|---|
| Server-side Session stores version | Multiple tabs, cross-operations between list and detail pages will overwrite each other |
| Add pessimistic lock when opening for editing | User leaving the page open without saving holds the lock for a long time, poor web experience |
Frontend passes version (current approach) |
Stateless, suitable for HTTP REST, simple to implement |
version is not a sensitive field but a concurrency control credential. Exposing it to the frontend is an industry standard (JPA @Version, MyBatis manual version + 1 all do this).
3. Practical Frontend-Backend Collaboration Example
The following uses "business documents" as an example to show how optimistic locking is implemented in a real project.
3.1 Backend: Entity and SQL
// Order.java
private Integer version;
<update id="updateWithVersion">
UPDATE biz_order
SET status = #{status},
receiver_name = #{receiverName},
update_time = NOW(),
version = version + 1
WHERE id = #{id}
AND version = #{version}
</update>
3.2 Which Write Operations Need to Carry version?
| Operation Type | Pass version? | Explanation |
|---|---|---|
| Edit main table fields | ✅ | Typical optimistic locking scenario |
| Batch rule recalculation | ✅ | Each record carries its own id + version |
| Split, merge | Depends on situation | Recommended if concurrency is frequent |
| Append remarks only | Optional | Consequences of conflict are usually acceptable |
3.3 Case A: List Page Batch Operation
const payload = selectedRows.map(row => ({
id: row.id,
version: row.version,
}));
await batchUpdate(payload);
3.4 Case B: Detail Page Edit and Save
await updateOrder({
id: form.id,
version: form.version,
receiverName: form.receiverName,
});
3.5 Case C: Cross-Operation Between List and Detail
If the detail page has been modified by someone else, the batch submit from the list page will match 0 rows with UPDATE WHERE version = ?, requiring a refresh and retry.
4. Frontend Type Differences Between version and id
When integrating orders, IDs uniformly use string, while version still uses number. The reasons differ:
| Field | Typical Value Range | Frontend Type | Reason |
|---|---|---|---|
id (Snowflake ID) |
15-19 digit large integer | string |
Exceeds JS Number.MAX_SAFE_INTEGER (2^53-1), converting to number loses precision |
version |
0, 1, 2 … usually < 1000 | number |
Small integer, no precision issue, passing as number in JSON is fine |
Will version Overflow?
The Integer upper limit is about 2.1 billion. For business documents:
- Updating 100 times/day → takes about 58,000 years to overflow
- Updating 1000 times/day → takes about 5,800 years
Document-type scenarios have no practical overflow risk. If still concerned, the backend can upgrade the field to BIGINT.
5. Suitable Scenarios for Optimistic Locking
✅ Suitable for Optimistic Locking
- Business documents: Purchase plans, orders, purchase orders, contracts
- Form editing: Viewable by multiple people, occasionally modified, low conflict probability
- Read-heavy, write-light: Mainly list browsing, write operations are relatively sparse
- No long transactions: User edits and submits, no need to hold a lock for a long time
- Retry is acceptable: Prompt refresh after conflict, user re-operates
⚠️ Situations Optimistic Locking Cannot Prevent
- Interfaces that don't go through version validation (e.g., some remark or split interfaces)
- Only validating version, not business state (e.g., a plan already "completed" is still mistakenly operated — requires additional state machine validation)
- Frontend doesn't refresh after conflict, continues retrying with old version
- Read-modify-write spans multiple interfaces without transactions (intermediate state might be inserted by others)
6. Detailed Scenarios for Pessimistic Locking
When conflict probability is high, "failure prompting user to refresh and retry" is unacceptable, or resources must be exclusively held immediately after reading, pessimistic locking (or equivalent atomic SQL / distributed lock) should be prioritized.
6.1 When Should You Use Pessimistic Locking?
If any of the following conditions are met, you usually shouldn't rely solely on optimistic locking:
| Judgment Condition | Explanation |
|---|---|
| High conflict probability | The same row of data is modified concurrently multiple times in a short period (inventory, balance, hotspot SKU) |
| Must succeed | Business does not allow "version conflict, please refresh" — e.g., payment deduction, outbound confirmation |
| Read-then-write, very short interval | No intermediate state of "user staring at form for half an hour," but read and immediately modify within the interface |
| Limited resource quantity | Inventory, slots, quotas, document numbers — resources that "run out once grabbed" |
| Cross-service serialization | Multiple microservices must execute mutually exclusively on the same business key (settlement generation, reconciliation batch) |
| Critical state machine nodes | Approval submission, shipment confirmation — state transitions that "can only succeed once" |
6.2 Problems Pessimistic Locking Solves (Different from Optimistic Locking)
Optimistic locking prevents: Old snapshot overwriting new data (Lost Update).
Pessimistic locking also prevents:
- Overselling: 1 item in stock, two people successfully place orders simultaneously
- Duplicate deduction: The same payment callback is processed twice
- Duplicate generation: The same settlement group concurrently generates two reports
- Wrong decisions caused by dirty reads: Reading an intermediate state and then continuing to write based on wrong data
6.3 Typical Suitable Scenario Categories
A. Inventory and Warehousing
| Scenario | Why Pessimistic Lock / Atomic Operation | Common Implementation |
|---|---|---|
| Inventory deduction / reservation / release | Extremely high concurrency, optimistic locking causes many failed retries, poor experience and performance | Atomic SQL: UPDATE stock SET qty = qty - n WHERE qty >= n |
| Unique code inventory occupation | Same uniqueKey contested by multiple orders |
version optimistic lock + unique key constraint |
| Location allocation / picking lock | Same location cannot be assigned to two orders | SELECT ... FOR UPDATE or Redis lock |
| Inventory count adjustment | Prohibit other inbound/outbound during adjustment | Business lock + status field "Counting" |
Note: Inventory scenarios sometimes use atomic SQL instead of explicit locking, but the essence is still a pessimistic approach — assume conflict before updating, decide success/failure in one DB-level operation.
B. Funds, Points, and Settlement
| Scenario | Why Pessimistic Lock | Common Implementation |
|---|---|---|
| Account balance changes | Amount must never be wrong, cannot let user "refresh and retry" | Row lock FOR UPDATE or account-level Redis lock |
| Point increment/decrement | Same logic as balance | Same as above |
| Settlement report generation | Same tenantId + sellerId + financialEventGroupId can only run once |
Redis distributed lock (report-service) |
// Distributed lock example
String lockKey = "settlement:generate:" + tenantId + ":" + bizGroupId;
if (!redisLock.tryLock(lockKey, 600)) {
throw new ServiceException("Failed to acquire distributed lock for settlement generation");
}
try {
return doGenerate(request);
} finally {
redisLock.unlock(lockKey);
}
C. Flash Sales, Rush Purchases, Limited Resources
| Scenario | Characteristics | Implementation Key Points |
|---|---|---|
| Flash sale order | Instant tens of thousands of concurrent requests for a small amount of stock | Redis pre-deduction + DB eventual consistency; or token bucket rate limiting |
| Coupon claiming | Total quantity limited | Atomic decrement + unique index to prevent duplicate claims |
| Event quotas | Gone once grabbed | Pessimistic lock or Lua script atomic judgment |
In these scenarios, optimistic locking would cause a large number of request failures, unsuitable for web form-style retries.
D. Critical Document Nodes (Short Transaction Pessimistic Lock)
Unlike "user opens form and slowly edits," these are short transactions within an interface:
| Scenario | Approach |
|------|------|----------|
| Batch approval submission | Lock rows first, then change status |
| Shipment confirmation / outbound posting | FOR UPDATE + status validation |
| Ordering-in-progress status | Prevent duplicate orders, change to "ordering" first then call external systems |
// OrderRepository.java
queryWrapper.in(OrderEntity::getId, ids).last("FOR UPDATE");
E. Scheduled Tasks, Message Consumption, Batch Processing
| Scenario | Why Lock is Needed | Implementation |
|---|---|---|
| Scheduled task single instance | Only one instance can run when deployed on multiple nodes | Distributed lock / DB lock table |
| MQ consumption idempotency | Duplicate messages must not execute business logic repeatedly | Business unique key + distributed lock |
| Reconciliation / report generation | Concurrent recalculation of same dimension data prohibited | Redis lock (see settlement report generation) |
| Document number generation | Prevent concurrent duplicate numbers | DB sequence / Redis INCR / number segment lock |
F. Scenarios Clearly Unsuitable for Pessimistic Locking (Reverse List)
The following scenarios should not use database pessimistic locks (FOR UPDATE) for long holds:
| Scenario | Reason | Should Use Instead |
|---|---|---|
| User opens form to edit for half an hour | Lock held too long, blocks others | Optimistic lock version |
| Batch check and operate from list page | Cannot predict when user will submit | Optimistic lock, validate at submit time |
| Pure query, report browsing | No lock needed | No lock |
| Appending logs, remarks only | Conflict consequences acceptable | Optimistic lock or no lock |
6.4 Three Common Implementations of Pessimistic Locking
flowchart TD
A[Need Pessimistic Lock?] --> B{Lock Scope}
B -->|Single DB, single row| C["Database Row Lock<br/>SELECT ... FOR UPDATE"]
B -->|Single DB, multiple rows / complex deadlock prevention| D["Application-layer transaction + ordered locking"]
B -->|Cross-service / cross-node| E["Distributed Lock<br/>Redis / Redisson"]
C --> F{Only need to check quantity?}
F -->|Yes| G["Atomic SQL<br/>UPDATE ... WHERE qty >= n"]
F -->|No| C
| Implementation | Principle | Pros | Cons | Typical Scenario |
|---|---|---|---|---|
SELECT ... FOR UPDATE |
Locks queried rows within a transaction, other transactions wait | Strong consistency, intuitive implementation | Blocking, deadlock risk, unsuitable for long transactions | Approval submission, outbound confirmation |
| Redis / Redisson Distributed Lock | Mutual exclusion on business key | Cross-service, configurable timeout | Need to handle lock renewal, master-slave switchover edge cases | Settlement generation, scheduled tasks |
| Atomic SQL (Conditional Update) | Single UPDATE completes read-judge-write | Best performance, no explicit lock wait | Can only express simple conditions | Inventory deduction, quota decrement |
| Database Pessimistic Lock Annotation | JPA @Lock(LockModeType.PESSIMISTIC_WRITE) |
Integrated with ORM | Rarely used directly in MyBatis projects | JPA projects |
6.5 Pessimistic Lock Usage Notes
- Minimize lock granularity: Lock one row, not the table; lock the "inventory row," not the "entire order module"
- Must set timeout: Distributed locks set
leaseTime; DB locks rely on transaction timeout to avoid long-hanging deadlocks - Consistent locking order: When locking multiple rows, lock in a fixed order (e.g., by id sorting) to prevent deadlocks
- No slow IO inside lock: Calling third-party APIs, sending emails should be outside the lock or asynchronous, otherwise lock hold time explodes
- Can be combined with optimistic locking: e.g., inventory uses atomic SQL, document main table still uses
versionto prevent form overwrites - Frontend usually unaware: Pessimistic lock is transparent to the frontend; users only feel "slightly slower" or "queued," unlike optimistic locking which requires passing
version
6.6 Optimistic Locking vs. Pessimistic Locking: Selection Decision Tree
flowchart TD
Start[Concurrent Write Operation] --> Q1{Will user hold edit state for a long time?}
Q1 -->|Yes, minutes~hours| Opt[Optimistic Lock version]
Q1 -->|No, milliseconds~seconds within interface| Q2{If conflict fails, can user refresh and retry?}
Q2 -->|Yes| Q3{Is conflict probability very low?}
Q3 -->|Yes| Opt
Q3 -->|No, hotspot data| Pes[Pessimistic Lock / Atomic SQL]
Q2 -->|No, must succeed| Q4{Cross-microservice?}
Q4 -->|Yes| Redis[Redis Distributed Lock]
Q4 -->|No, single DB| DB[FOR UPDATE or Atomic SQL]
6.7 Lock Strategy Comparison for Common Business Scenarios
| Business Type | Lock Strategy | Reason |
|---|---|---|
| Document editing, review | Optimistic Lock version |
Form editing, few conflicts, can prompt refresh |
| Batch approval, shipment confirmation | Pessimistic Lock FOR UPDATE |
Batch status change within short transaction, cannot partially fail |
| Inventory detail increase/decrease | Optimistic Lock + unique key / Atomic SQL | High concurrency; combine with conditional update to prevent overselling |
| Settlement / report generation | Distributed Lock Redis | Prevent duplicate generation across tasks, must be mutually exclusive |
| Inventory deduction to prevent overselling | Atomic SQL | WHERE available_qty >= ? one-time judgment |
7. Frontend-Backend Collaboration Specification (Practice Checklist)
7.1 Backend
- Table structure adds
version INT/BIGINT NOT NULL DEFAULT 0 - All write operations that change main table state:
UPDATE ... WHERE id=? AND version=?, andversion = version + 1 - When
affected_rows = 0, return a clear error code (e.g., "version conflict / data has been modified"), do not silently succeed - Query interfaces (list, detail) return current version for frontend caching
- Clearly specify which write interfaces must carry version, which do not (documented)
7.2 Frontend
- Read and save
versionfrom interface when opening page (list rows, detail form) - Write request body carries the
versionat the time of reading, do not use0as a fallback to mask problems (unless backend agrees 0 has special semantics) - Upon receiving version conflict error: refresh data, operate with new
version, prohibit blindly retrying with old version -
idusesstring,versionusesnumber(for document scenarios) - List page batch operation: each record carries its own
id + version, cannot share one version
7.3 Product / Interaction
- Unified conflict prompt text: "Data has been modified by someone else, please refresh and retry"
- Optional: Auto
refresh()after conflict and preserve user input (advanced experience, higher implementation cost)
8. Common Implementation Quick Reference
| Implementation | Description | Suitable Scenario |
|---|---|---|
| Version Number Field | version integer increment |
Orders, contracts, approval forms |
| Timestamp | update_time as version basis |
Some lightweight systems, WHERE update_time = ? |
JPA @Version |
ORM auto-management | Spring Data JPA projects |
| MyBatis Manual | version + 1 in SQL |
Traditional Java backend projects |
| Database Row Lock | SELECT ... FOR UPDATE |
Short transaction batch status changes |
| Redis Distributed Lock | tryLock / Redisson |
Settlement report generation |
| Atomic Conditional Update | UPDATE ... WHERE qty >= n |
Inventory overselling prevention |
9. Distinction from Related Concepts
| Concept | Purpose | Relationship with Optimistic Lock |
|---|---|---|
| Optimistic Lock version | Prevent old data from overwriting new data | Topic of this article |
| Idempotency | Repeated submission of same request yields same result | Complementary; after optimistic lock failure, can safely retry (need to refresh version) |
| Distributed Lock | Cross-service/cross-instance mutual exclusion | Pessimistic approach, used for higher conflict scenarios |
| Transaction Isolation Level | Database read-write isolation | Optimistic lock is at application layer, does not depend on RR/Serializable |
| Business State Machine | Restrict "what can be done in what state" | Used in combination with optimistic lock, each managing one layer |
10. One-Sentence Summary
Optimistic Lock = At commit time, check "is the version you saw still the latest?"; suitable for form editing, documents with few conflicts.
versionis a concurrency credential given to the frontend.Pessimistic Lock = Occupy first before operating, others wait or fail; suitable for inventory, funds, flash sales, batch processing — scenarios with high conflict where users cannot be asked to refresh and retry.
In real projects, they are often combined: document editing uses optimistic lock version; batch approval uses FOR UPDATE; cross-service batch processing uses Redis distributed lock; inventory deduction uses atomic SQL. Snowflake id uses string on the frontend, version uses number.
11. References
- JPA Specification:
@Versionannotation (javax.persistence.Version) - JPA Specification:
@Lock(PESSIMISTIC_WRITE)pessimistic lock - Classic Problems: Lost Update, Overselling, Deadlock
- Redisson Distributed Lock Documentation
Document created: 2026-07-16