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:

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.

JAVA
@Entity
public class ProductWithVersion {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private Long version;

}
Click to expand and view more

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!

Copyright Notice

Author: MigueC

Link: https://miguec.dev/posts/locking-concepts/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut