The Problem: Concurrent Updates
When multiple users try to update the same data simultaneously, conflicts can arise. Let’s see how this happens with Alice and Bob:
Keep in mind that Users are equivalent to different threads trying to update the same DB record at the same time.
sequenceDiagram participant A as Alice participant DB as Database participant B as Bob Note over A,B: Both users want to update the same record A->>DB: Read account balance: $100 B->>DB: Read account balance: $100 Note over A: Alice wants to withdraw $30 Note over B: Bob wants to deposit $50 A->>A: Calculate: $100 - $30 = $70 B->>B: Calculate: $100 + $50 = $150 A->>DB: Update balance to $70 B->>DB: Update balance to $150 Note over DB: 💥 Conflict! Bob's update overwrites Alice's Note over DB: Final balance: $150 (should be $120)
The Solution: Locking Mechanisms
There are two mechanisms to solve this issue:
- Optimistic locking
- Pessimistic locking
In this post, I’ll focus on Optimistic locking because it performs better than pessimistic locking when conflicts are infrequent. Which means, read operations are more frequent than write operations.
Why, does it perform better? Because it avoids unnecessary waiting and locking for users.
Optimistic Locking Approach
Optimistic locking approach is achieved by using a version field in the entity definition. The version will be increased every time the record gets updated.
sequenceDiagram participant A as Alice participant DB as Database participant B as Bob Note over A,B: Optimistic Locking with Version Control A->>DB: Read account (balance: $100, version: 1) B->>DB: Read account (balance: $100, version: 1) A->>A: Calculate: $100 - $30 = $70 B->>B: Calculate: $100 + $50 = $150 A->>DB: Update if version=1: balance=$70, version=2 DB-->>A: ✅ Success! Updated to version 2 B->>DB: Update if version=1: balance=$150, version=2 DB-->>B: ❌ Failed! Version mismatch (current version is 2) B->>DB: Re-read account (balance: $70, version: 2) B->>B: Recalculate: $70 + $50 = $120 B->>DB: Update if version=2: balance=$120, version=3 DB-->>B: ✅ Success! Final balance: $120
Java details
@Version annotation
The @Version
annotation needs to be added to a field of our root entity.
@Entity
public class ProductWithVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
}
Then, Hibernate will handle the field by us, we don’t need to set or update this value.
Every time a save
is performed, JPA will do the version check and if the version doesn’t match with the version in the DB it will trigger and
ObjectOptimisticLockingFailureException
exception. Then, it is the application responsability to decide what to do after the exception. It could be informed to the user to perform another attempt, or it could be retried in behalf of the user.
Something to keep in mind is that, if an update is going to set up the exact same details than the stored object, the update will not be executed. So, the version will not be bumped.
I’ve created a repository showcasing what was described above, you can check it here: https://github.com/miguecdev/java-locking-demo
Thanks for reading!