跪拜 Guibai
← Back to the summary

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:


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:

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

⚠️ Situations Optimistic Locking Cannot Prevent

  1. Interfaces that don't go through version validation (e.g., some remark or split interfaces)
  2. Only validating version, not business state (e.g., a plan already "completed" is still mistakenly operated — requires additional state machine validation)
  3. Frontend doesn't refresh after conflict, continues retrying with old version
  4. 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:

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

  1. Minimize lock granularity: Lock one row, not the table; lock the "inventory row," not the "entire order module"
  2. Must set timeout: Distributed locks set leaseTime; DB locks rely on transaction timeout to avoid long-hanging deadlocks
  3. Consistent locking order: When locking multiple rows, lock in a fixed order (e.g., by id sorting) to prevent deadlocks
  4. No slow IO inside lock: Calling third-party APIs, sending emails should be outside the lock or asynchronous, otherwise lock hold time explodes
  5. Can be combined with optimistic locking: e.g., inventory uses atomic SQL, document main table still uses version to prevent form overwrites
  6. 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

7.2 Frontend

7.3 Product / Interaction


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. version is 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


Document created: 2026-07-16