In the previous post, we discussed the theory behind the indexes used in a DBMS to improve the data searching on a database table.
This new post will discuss the “Optimistic Logic” technique and how to implement it using Hibernate.
Table of Contents
What is the Optimistic Locking?
Optimistic Locking is a technique used in computer science to manage concurrent access to shared resources, such as databases or files, by multiple users or processes. The basic idea behind optimistic Locking is to assume that conflicts between concurrent accesses are rare. Therefore, allow multiple users or processes to access the resource simultaneously and only check for conflicts when attempted updates are made.
When a user or process begins to access a shared resource, it reads the current version, including a version or timestamp. The user or process then performs its work and rechecks the current version when ready to update the resource to ensure that no other user or process has updated it. If the version has changed, the user or process must reconcile the changes before it can apply its update.
When should we use Optimistic Locking?
Optimistic Locking is used in various applications and systems, including database management, version control, and web applications. One common use case is in e-commerce websites, where multiple users may be attempting to purchase the same item simultaneously. Optimistic Locking can prevent overselling the item by allowing multiple users to add it to their cart simultaneously but only allowing one user to purchase it.
What challenges the Optimistic Locking brings?
There are several challenges to implementing Optimistic Locking effectively. One challenge is ensuring that the version or timestamp used to detect conflicts is accurate and reliable. Conflicts may not be detected if the version is not updated correctly or if the system clocks on different servers are not synchronized. Another challenge is managing conflicts when they do occur. In some cases, conflicts can be resolved automatically, but in other cases, manual intervention may be required.
How to implement Optimistic Locking using Hibernate?
Hibernate is a popular object-relational mapping (ORM) framework for Java, and JPA (Java Persistence API) is a specification for ORM in Java. Both Hibernate and JPA support optimistic locking through a version attribute.
@Version Annotation
In Hibernate, optimistic locking is implemented by adding a version attribute to the entity class. This version attribute is typically an integer or a timestamp updated each time the entity is modified. When an entity is persisted or updated, Hibernate checks the version of the entity to see if it has changed since the entity was last read. If the version has changed, Hibernate throws an exception indicating that a concurrent modification has occurred.
You can use the annotation on the version attribute to enable optimistic locking in Hibernate. Here’s an example:
@Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Double price; @Version private Integer version; // getters and setters }
In this example, the Product
entity has a version attribute that is annotated with @Version
. When the entity is persisted or updated, Hibernate will check the value of the version attribute to detect concurrent modifications.
@OptimisticLocking Annotation
You can get Hibernate to perform “version checks” using either all of the entity’s attributes or just the attributes that have changed. This is achieved through the @OptimisticLocking annotation, which defines a single attribute of type org.hibernate.annotations.OptimisticLockType.
Options:
OptimisticLockType.ALL
: Specifies that all fields should be used for optimistic locking.OptimisticLockType.DIRTY
: Specifies that only fields that have been modified should be used for optimistic locking.OptimisticLockType.NONE
: Optimistic locking should not be used even if the @Version annotation is defined.OptimisticLockType.
VERSION: Specifies that the version attribute should be used for optimistic locking. This is the default option if not specified.
It’s worth noting that the OptimisticLockType.DIRTY
option can improve performance by reducing the amount of data that needs to be checked for changes during optimistic locking. However, it can also lead to incorrect results if changes to non-versioned fields are missed. The OptimisticLockType.ALL
and OptimisticLockType.VERSION
options provide more reliable optimistic locking but can be slower and use more memory.
Overall, the choice of an optimistic locking strategy depends on the application’s specific requirements and the acceptable performance trade-offs.
@Entity @OptimisticLocking(type = OptimisticLockType.DIRTY) public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Double price; @Version private Integer version; // getters and setters }
How to implement the Optimistic Lock in SQL?
Assuming we have a table called customers
with columns id
, name
, and version
(which is the version number used for optimistic locking), we can write a SQL statement to update a customer’s name while also checking the current version number:
UPDATE customers SET name = 'New Name', version = version + 1 WHERE id = 1 AND version = 2;
In this example, we update the column
to “New Name” and increment the version
column by 1. We also use a WHERE
clause to limit the update to the row with an id equal to 1 and a version equal to 2. This ensures the update only occurs if the current version number matches the expected value.
If the version number does not match, the update will not occur, and the SQL statement will return a result indicating that zero rows were affected. The application can use this to detect and handle optimistic locking conflicts.
What is the @OptimisticLockException?
The OptimisticLockException
is an exception thrown by JPA providers like Hibernate, EclipseLink, or OpenJPA when there is a concurrent modification of a database entity by two or more transactions that use optimistic locking.
When a transaction updates a row, it checks if the version in the database matches the version it has, and if it does, it updates the row and increments the version. If the version in the database has changed since the transaction read the row, it means that another transaction has updated the row, and the current transaction cannot update it.
When two transactions try to modify the same row concurrently, one will succeed, and the other will receive an OptimisticLockException
when it tries to commit. This exception signals that the data has been modified by another transaction during the time the current transaction was working with it, and it’s no longer safe to update it.
To handle this exception, the application needs to implement a mechanism to resolve the conflict, for example, by reloading the data and retrying the transaction or notifying the user that the update failed due to a concurrent modification. It’s crucial to handle optimistic lock exceptions properly to avoid corrupting the data and to provide a good user experience.
How to use the Optimistic Locking in a real use case?
Here is an example that uses JPA and the @Version
annotation for optimistic locking:
try { entityManager.getTransaction().begin(); // Find the customer entity Customer customer = entityManager.find(Customer.class, customerId); // Update the customer's information customer.setName("John Smith"); customer.setEmail("john.smith@example.com"); entityManager.merge(customer); entityManager.getTransaction().commit(); // Send email if commit was successful sendEmail(customer); System.out.println("Customer updated successfully."); } catch (OptimisticLockException ex) { // Data has been modified by another transaction, handle the conflict System.out.println("Unable to update customer due to optimistic lock conflict."); } catch (Exception ex) { // Other exception occurred, handle it accordingly entityManager.getTransaction().rollback(); System.out.println("Error occurred while updating customer: " + ex.getMessage()); } ... private void sendEmail(Customer customer) { // Implement the email sending logic here // ... System.out.println("Email sent successfully to " + customer.getEmail()); }
The code uses and the @Version
annotation to implement optimistic locking. Here’s what’s happening:
- The transaction begins.
- The
Customer
entity with the specified ID is retrieved from the database usingentityManager.find()
. The@Version
annotation ensures that the version number of the entity is also retrieved. - The customer’s information is updated.
- The
entityManager.merge()
is called to merge the changes made to theCustomer
entity with the corresponding entity in the persistence context. - If the
merge()
operation succeeds, the transaction is committed. - If the transaction is committed successfully, the
sendEmail()
method is called to email the updated customer. - Otherwise, if the
merge()
operation fails due to an optimistic lock exception, the transaction is rolled back, and an error message is printed to the console.
The use of the @Version
annotation means that the version number of the Customer
entity is automatically incremented by Hibernate every time the entity is updated. This version number is used by Hibernate to implement optimistic locking. When the merge()
operation is called, Hibernate checks the version number of the entity in the database against the version number of the entity in the persistence context. If the two version numbers are different, it means that the entity has been updated by another transaction since it was last retrieved by this transaction, and an optimistic lock exception is thrown. This allows the application to detect conflicts and handle them appropriately.
@Entity public class Customer { // Other fields and annotations here... @Version private Long version; // Getters and setters here... }
When Hibernate persists in an entity with a version field annotated with @Version,
it will automatically increment the version number every time the entity is updated in the database. If two transactions attempt to update the same entity simultaneously, one will succeed, while the other will receive an OptimisticLockException
because the version number in the database will not match the version number of the updated entity.
What is the difference between the Optimistic and Pessimistic locking strategies?
Optimistic locking assumes that conflicts between transactions are rare and does not lock the data until the transaction commits. Instead, it uses a versioning mechanism to ensure the data has not been modified since the transaction read it. If another transaction modifies the data before the first transaction attempts to write it, the optimistic lock mechanism detects the conflict and throws an OptimisticLockException. The application can then retry the transaction or handle the conflict differently.
Pessimistic locking, on the other hand, assumes that conflicts are more likely to occur and locks the data before a transaction reads or writes it. Pessimistic locking can use shared locks (allowing multiple transactions to read the same data but not write it) or exclusive locks (preventing other transactions from accessing the data). While pessimistic locking can prevent conflicts, it can also lead to decreased concurrency and performance issues.
In general, optimistic locking is preferred when conflicts are rare and the performance overhead of locking is a concern, while pessimistic locking is preferred when conflicts are common, and the cost of locking is outweighed by the benefits of preventing conflicts.
Thanks for reading. Stay tuned!
Learn. Grow. Teach.