Optimistic and Pessimistic Transaction Locks in Spring Boot Data JPA
Introduction#
Optimistic Locking and Pessimistic Locking are two approaches to manage concurrent access to data in a Spring Boot application using JPA. They help to prevent data inconsistencies that can arise from concurrent transactions, each trying to modify the same data simultaneously.
- Optimistic Locking: Assumes conflicts are rare, using a version field (
@Version
) to detect concurrent modifications. If a conflict occurs (i.e., the version is outdated), anOptimisticLockException
is thrown. It is ideal for scenarios where performance is prioritized over strict concurrency control. - Pessimistic Locking: Locks data to prevent conflicts. There are two types:
- Pessimistic Read: Prevents other transactions from modifying locked data.
- Pessimistic Write: Prevents both reading and writing to the locked data. It is suitable for cases where data conflicts are frequent and strict consistency is required, but it impacts performance.
These locking mechanisms help manage concurrency, with optimistic locking favoring performance and pessimistic locking ensuring stricter data integrity.
Conflicts in Concurrent Transactions#
In a database system, concurrent transactions occur when two or more transactions are executed simultaneously. When these transactions interact with the same data, conflicts may arise, potentially leading to data inconsistency or incorrect results. These conflicts are primarily due to issues related to concurrency control.
When multiple transactions run simultaneously, they may conflict, leading to issues like:
- Dirty Read: A transaction reads uncommitted data from another, leading to inconsistencies if the other transaction is rolled back.
- Non-Repeatable Read: A transaction reads the same data multiple times, but the data changes during the transaction due to another transaction.
- Phantom Read: A transaction's result set changes mid-execution as records are added or deleted by another transaction.
- Lost Update: Two transactions modify the same data simultaneously, causing one update to be overwritten and lost.
Isolation levels (e.g., READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE) and locking mechanisms help manage these conflicts, ensuring data consistency and integrity.
Transaction Locks#
Transaction locks in Spring Data JPA are used to ensure data consistency in environments with concurrent transactions. When multiple transactions try to access and modify the same data, these locks help prevent conflicts and ensure that data is not modified in an inconsistent manner.
There are two types of locks used in Spring Data JPA:
- Pessimistic Locking: Locks data to prevent other transactions from accessing it. This is like reserving a table at a restaurant; only the person who made the reservation can sit there, and others are blocked until they leave.
- Pessimistic Read: The data can be read but not modified by other transactions.
- Pessimistic Write: The data is locked for both reading and modifying.
- Optimistic Locking: Assumes there will be no conflicts and uses a version field to detect if someone else modified the data. This is like buying a concert ticket, assuming no one else buys it before you complete the transaction. If someone else does, your purchase fails.
How it works:
- When a transaction holds a lock on an entity, other transactions cannot access or modify the locked entity until the transaction holding the lock completes.
- Locks ensure that changes are committed safely and that other transactions are prevented from making conflicting changes simultaneously.
These locks help in concurrent environments, preventing issues like dirty reads, non-repeatable reads, and lost updates, ensuring data integrity.
Why use Transaction Locks#
While Isolation Levels define the visibility and consistency guarantees in concurrent transactions, Transaction Locks are necessary to enforce these guarantees and prevent conflicts during data access.
- Isolation Levels set the rules for transaction behavior, such as how one transaction can see changes made by others (e.g., dirty reads, phantom reads).
- Transaction Locks are the mechanisms used to enforce these rules by controlling access to data during transaction execution. They ensure that when an isolation level is set, the database can actually prevent conflicting transactions from occurring (e.g., preventing multiple transactions from modifying the same data simultaneously).
Why Locks are Necessary:#
- Conflict Prevention: Locks prevent different transactions from interfering with each other by restricting access to data.
- Complex Concurrency Handling: In real-world scenarios, concurrency issues like dirty reads or lost updates can be complex to manage. Locks provide the practical tools to handle these issues by controlling access to data at a granular level.
- Guarantee Enforcement: Isolation levels define how transactions should behave, but locks make sure that these behaviors are implemented correctly and consistently.
In essence, isolation levels define how transactions should interact with each other, while transaction locks provide the mechanisms that ensure those rules are followed during execution.
Optimistic Locking#
Optimistic locking ensures that data integrity is maintained without locking the entire row or table, allowing for better concurrency. Here's how it works step by step:
- Add a Version Column: A new column called "version" is added to the database table. This column stores the version number of each row.
- Read Version Number: When a user wants to modify a row, the application first reads the current version number of that row.
- Update Row with New Version: When the user updates the row, the application increments the version number by 1 and writes the updated data back to the database.
- Validation Check: The database checks whether the version number matches the expected value (i.e., the version number in the database should exceed the previous version number by 1). If this check fails (meaning another transaction has modified the data in the meantime), the transaction is aborted. The user will then be asked to repeat the process from step 2.
Imagine a shared document where several people can edit. Before making changes, you check the current version of the document. If someone else modifies the document in the meantime, your changes won't be applied until you synchronize again, ensuring that no conflicting edits happen.
Implementation:#
Entity Code: Use @Version
Repository Code:
Controller Code:
Service Code: No @Transactional
is used, operating under default READ_COMMITTED
isolation.
Concurrency Test Output#
Output of bash terminal:
After creating the employee if we are trying to hitting put mapping for 100 times using the command. This command uses seq to make the PUT curl request 100 times. It uses xargs -P flag to call 10 curl requests at a time.
Command#
Run 100 concurrent requests to increment the balance:
seq 100 | xargs -P 10 -I {} curl --location --request PUT ‘http://127.0.0.1:8080/employees/incrementBalance/1’
Alternatively, You can also use ApacheBench (installation may be required)
ab -n 100 -c 10 -T 'application/json' -m PUT
http://127.0.0.1:8080/items/incrementCount/1
If you are not handling the exception you will get this exception(Internal Server).
The behavior is caused by concurrent access and lack of synchronization mechanisms during updates to the balance. Key points:
- Concurrent Reads and Writes: Multiple threads read the same initial state (
balance=1.00, version=1
) and try to update it simultaneously. - Outdated Data Overwriting: Threads using stale data (outdated
version
) overwrite newer updates, causing anomalies like the balance "jumping backward." - Stale Data Errors: Version mismatches lead to
Stale data
errors when conflicting updates occur. - No Transactions or Locking: Without transactional boundaries or optimistic locking, consistency cannot be maintained during concurrent updates.
Additional causes include potential caching issues, network delays, or concurrency conflicts leading to inconsistent or outdated data being processed.
Output of ide console:
At the point of ‘Stale data’ we got a error.
If you are not handling the exception you will get this exception.
Database:
Observations#
Database Integrity Maintained:
Despite multiple requests, the final balance reflects only successful updates.
Error Handling:
Some requests fail with "stale data" errors due to version mismatch, as optimistic locking prevents overwriting newer updates.
Advantages#
- High Concurrency: No table-level locks, better for read-heavy systems.
- Data Integrity: Prevents overwrites in concurrent environments.
Disadvantages#
- Retry Logic: Developers need to handle retries for failed updates.
- Performance Impact: More checks and retries may affect performance.
Pessimistic Locking#
Pessimistic locking is a concurrency control mechanism that prevents conflicts in a multi-transaction environment by explicitly locking data for the duration of a transaction.
- PESSIMISTIC_READ: This lock mode allows a transaction to read data but ensures that no other transaction can modify the data until the current transaction completes. It’s also known as a shared lock since other transactions can also read the data, but none can modify it.
- PESSIMISTIC_WRITE: This lock mode (also called an exclusive lock) prevents both reads and writes by other transactions on the locked row. Only the transaction holding the lock can modify the row, ensuring complete control over the data until the transaction is completed.
In Spring Data JPA, you can use the @Lock
annotation in the repository interface to specify these lock modes and control concurrent access to data.
Imagine a library book:
- PESSIMISTIC_READ: You can borrow the book, but others can still read it while you're using it.
- PESSIMISTIC_WRITE: You borrow the book and no one else can read or borrow it until you're done.
Implement:#
Entity Code:
Repository Code:
Controller Code:
Service Code:
Output:
Feature | Optimistic Locking | Pessimistic Locking |
---|---|---|
Lock Type | No upfront lock; relies on version checking | Locks data upfront, blocking other transactions |
Performance | Better in low contention scenarios | Better in high contention scenarios |
Deadlocks | Not possible | Possible, must be managed |
Overhead | Retries in case of conflict | Blocking and higher resource usage |
Best For | Read-heavy, low-conflict systems | Write-heavy, high-conflict systems |
Transaction Strategies in real world#
Here’s how different transaction strategies are applied in various real-world scenarios:
- Read Committed:
- Use Case: E-commerce, online transactions where you need consistent data but prioritize performance.
- Example: Amazon, where consistent and up-to-date data is important but high throughput is also a priority.
- Read Uncommitted:
- Use Case: Data analytics, non-critical reporting where slight inconsistencies are acceptable.
- Example: Google Analytics, where real-time accuracy may not be as critical and occasional anomalies don't significantly impact the analysis.
- Serializable:
- Use Case: Critical operations like financial transactions, stock trading, or banking where data integrity is paramount.
- Example: State Bank of India (SBI), Punjab National Bank (PNB), where strict consistency is necessary to prevent financial discrepancies.
- Optimistic Locking:
- Use Case: Collaborative editing environments where conflicts are rare but need to be managed without excessive locking.
- Example: Google Docs, where multiple users can edit the document simultaneously but are notified if someone else has made a conflicting change.
- Pessimistic Locking:
- Use Case: Systems requiring exclusive access to resources to avoid conflicts, such as booking systems.
- Example: Booking.com, airline reservations, where only one user can book a specific seat or room at a time to prevent overbooking.
- No Transactions:
- Use Case: Social media or large-scale distributed systems where eventual consistency is acceptable and transactions aren’t needed.
- Example: Facebook, Twitter, where rapid updates and massive scale are more important than strict transaction consistency, allowing for eventual consistency across the system.
These strategies help tailor database behavior to the specific needs of different applications, balancing consistency, performance, and user experience.
Conclusion#
This article compares Optimistic and Pessimistic transaction locks in Spring Boot Data JPA, highlighting their approaches to concurrency and data consistency. Optimistic Locking favors performance with versioning for rare conflicts, while Pessimistic Locking offers stricter control in high-conflict scenarios. Understanding both strategies helps developers build efficient and reliable applications that balance performance and data integrity.