Pessimistic and Optimistic Locks in Spring Boot Application
Pessimistic and Optimistic Locks
A lock is a mechanism for controlling access to shared resources by multiple threads.
Commonly, a lock provides exclusive access to a shared resource, i.e, only one thread at a time can acquire the lock and all access to the shared resource requires that the lock be acquired first. So, it ensures that only one thread will change the data.Generally it is a conflict among multiple threads to access a shared resource and that’s why a lock comes into play in such situation to allow only one thread to deal with such conflict.
To deal with such conflicts you have two options for locking mechanism – Pessimistic Locking mode where you try to avoid conflicts and Optimistic Locking mode where you can allow conflicts to occur, but you need to detect it upon committing your transaction.
Pessimistic locking achieves this goal by taking a shared or read lock on the target object so that other threads are prevented from performing operations on the target object except the one that has acquired the lock.
Optimistic lock assumes that nothing’s going to change while you’re reading it. Pessimistic lock assumes that something will and so locks it.
To select proper locking mechanism you have to estimate the amount of reads and writes and plan accordingly.
Example
Let’s consider a bus database where travellers information with bus data are stored. The bus table stores information about buses, and tickets stores information about booked tickets. Each bus has its own capacity, which is stored in the bus.capacity column. Our application should control the number of tickets sold and should not allow purchasing a ticket for a fully occupied bus. To do this, when booking a ticket, we need to get the capacity of the bus and the number of tickets sold from the database, and if there are empty seats on the bus, sell the ticket, otherwise, inform the user that the seats have run out. If each user request is processed in a separate thread, data inconsistency may occur. Suppose there is one empty seat on the bus and two users book tickets at the same time. In this case, two threads simultaneously read the number of tickets sold from the database, check that there is still a seat left, and sell the ticket to the client. In order to avoid such collisions, locks are applied.
Handling data integrity is paramount in the situation where concurrent database operations are being performed. Spring Data JPA offers both optimistic and pessimistic locking mechanisms.
Optimistic Locking with Spring Data JPA
The optimistic approach is based on assumption that a data conflict is rare. Hence instead of locking the data, it checks only when data is actually updated. This is achieved through a version column in the entity.
@Entity
public class Bus {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private int version;
private int capacity;
}
public interface BusRepository extends JpaRepository<Bus, Long> {}
@Service
public class BusService {
@Autowired
private BusRepository busRepository;
public void updateCapacity(Long id) {
Bus bus = busRepository.findById(id).orElseThrow();
bus.setCpacity(bus.getCapacity() - 1);
busRepository.save(bus);
}
} In the above example, when multiple threads try to update the capacity in the same bus simultaneously, only the first thread will be able to successfully update the capacity. The second thread will fail to update as the version will not match and it will throw an exception ObjectOptimisticLockingFailureException.
Generated SQL for updateCapacity() method could be as follows:
SELECT id, capacity, version FROM bus WHERE id = ?
UPDATE bus SET capacity = ?, version = ? WHERE id = ? AND version = ? With Optimistic Locking (as shown in the above example using the @Version annotation):
Read operations (like findById()) are non-blocking and can be done in parallel by multiple threads. They don’t care about the version column.
Write operations (like save) will check the version column to ensure that the data has not changed since it was read. If another thread has updated the data in the meantime (and hence, incremented the version number), the save operation will fail with an ObjectOptimisticLockingFailureException.
In cases where you would like to ensure that the read operation is the latest or block other operations while reading, you will need to employ pessimistic locking strategies like PESSIMISTIC_READ or PESSIMISTIC_WRITE.
Pessimistic Locking with Spring Data JPA
Pessimistic locking mechanism assumes that the contention for a data item will happen and thus simultaneous update is prevented by locking the item for the duration of a transaction. This approach guarantees that once a thread acquires a lock, no other thread can access the data until the lock is released.
@Entity
public class Bus {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int capacity;
}
public interface BusRepository extends JpaRepository<Bus, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Bus> findByIdLocked(Long id);
}
@Service
public class BusService {
@Autowired
private BusRepository busRepository;
@Transactional
public void updateCapacity(Long id) {
Bus bus = busRepository.findByIdLocked(id).orElseThrow(EntityNotFoundException::new);
bus.setCpacity(bus.getCapacity() - 1);
busRepository.save(bus);
}
} The @Lock(LockModeType.PESSIMISTIC_WRITE) annotation ensures that a write lock is obtained when findByIdLocked() is invoked.
Here, the @Transactional annotation starts a new transaction when updateCapacity() method is invoked. If the method completes successfully, the transaction commits, and if an exception occurs, it rolls back.
Generated SQL for updateCapacity() method could be as follows:
SELECT id, capacity FROM bus WHERE id = ?
UPDATE bus SET capacity = ? WHERE id = ? Pessimistic locking, as shown in the above example using Spring Data JPA, prevents concurrent data access conflicts by obtaining a lock for the entire duration of the transaction. This method is particularly beneficial in high-contention scenarios. However, it is essential to be aware of the potential for deadlocks and the impact on system throughput. Proper transaction management, as showcased with @Transactional, ensures the atomicity of operations.
Depending on specific use cases and performance requirements, developers can choose between pessimistic and optimistic locking mechanism. Each method comes with its unique advantages and challenges. The decision could be taken based on the frequency of concurrent data access and the desired level of strictness in managing data consistency.
No comments
Leave a comment