Pessimistic and Optimistic Locking in JPA, Spring Boot

Berat Yesbek
8 min readDec 25, 2023

--

We are going to cover pessimistic and optimistic locking mechanisms in JPA.

Before jumping into the JPA and Spring Boot side, it’s crucial to understand what pessimistic and optimistic locking are, along with their respective advantages and disadvantages.

Ensuring data integrity and effectively managing locking mechanisms without interrupting ongoing actions is one of the most challenging aspects of the concurrency process.

Consider isolation levels in the databases a potential solution. And you are absolutely right. However, there are two approaches. The first involves utilizing isolation levels configured during the database connection as you thought. The second option is to lock the data that is being processed by a transaction.

Isolation levels are defined at the database connection level and come into play during the configuration phase.

Pessimistic Locking

While a data row is being processed, it is locked, and during this time, no other transaction can modify the current record. This locking mechanism is session-based, meaning that a data row is locked from the beginning of a transaction until the session is closed, whether through committing or rolling back the transaction.

According to the scenario, Alice aims to deposit money from her stock market earnings into her bank account. At the same time, Phill, realizing he hasn’t paid this month’s rent, intends to deposit the rent into Alice’s bank account. Phill initiates the process using an ATM, and Alice uses a mobile banking app. Both send their requests to the server simultaneously and Transaction A is created for Phill, and Transaction B is created for Alice.

Both transactions wish to access the same data row. Transaction A accessed this data row asking the Police in the Department of Pessimistic Locking. The police review indicates that no ongoing actions involve the data row, allowing Transaction A to access and modify the data row associated with Alice’s bank account. Meanwhile, Transaction B also asks to Police, “Can I access this data row which belongs to Alice to read or modify”. The police respond. “Dear Transaction B, you cannot currently access it until Transaction A completes its process. Alice’s bank account has been locked by Transaction A, and no further actions can be performed until it is released”

In summary, if one transaction has already accessed a data row, other transactions must wait until it is released by Transaction A. During this process, the data row is locked. This phenomenon is called Pessimistic Locking, designed to ensure data integrity and consistency. While pessimistic locking is effective in maintaining data consistency and integrity, it also introduces the risk of potential deadlock issues.

Optimistic Locking

In enterprise applications, concurrent access to the database is crucial. Applications must perform transactions independently without locking, ensuring data integrity and consistency. Optimistic Locking allows two threads to read or modify the same data row without blocking each other. However, there is a problem to consider with this approach.

For instance, Alice has a bank account with a balance of $32,000. She wants to transfer $10,000 from a different account to hers using a mobile application. Simultaneously, Sarah realizes she forgot to pay rent for the month and decides to pay $1,000 using an ATM. Both submit their requests to the server simultaneously, Transaction A being created for Alice and Transaction B for Sarah.

Transaction A and Transaction B can modify data at the same time. Transaction B updates Alice’s account balance from $32,000 to $33,000 due to Sarah’s transaction. However, Alice still sees her balance as $32,000, assuming she updated it from $32,000 to $42,000. Is this correct? Hmm, not actually. Alice updated her balance from $33,000 to $43,000 because changes have not been yet committed by the transaction. This phenomenon is known as Stale Data.

We would expect Alice to update her bank account balance from $32,000 to $42,000 because she deposited $10,000. However, since Sarah also deposited $1,000 at the same time, Alice’s balance actually updated from $32,000 to $33,000. Later, when Alice checks her balance, she assumes it was updated from $32,000 to $42,000, but in reality, it was updated from $33,000 to $43,000 due to Sarah’s transaction

How to solve this problem. We can prevent the second Transaction (user) from updating stale data. We can solve this problem by adding the version column. Optimistic Locking detects changes on the data by checking the version column. It is suitable If the service has many read-and-write operations. We can provide this using @Version annotation in Spring Boot.

Pessimistic Locking in JPA

JPA provides three lock modes. PESSIMISTIC_READ, PESSIMISTIC_WRITE and PESSIMISTIC_FORCE_INCREMENT.

PESSIMISTIC_READ

It provides a shared lock, meaning that multiple transactions can read the same record simultaneously. However, it prevents the acquisition of a write lock on the same record. In summary, any transaction can read simultaneously, but updates or deletes are not allowed. The record will be blocked until it is released for modification. You must start a transaction in service or on method, before the method call. @Transactional. Otherwise, it will throw an exception.

public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Override
@Lock(value = LockModeType.PESSIMISTIC_READ)
Optional<Category> findById(Integer integer);
}

org.hibernate.SQL : select c1_0.id,c1_0.name,c1_0.version from category c1_0 where c1_0.id=? for share

As you can see from logs, Pessimistic Read only allows data to be shared (read).

PESSIMISTIC_WRITE

It provides an exclusive lock, meaning that only one transaction can read or write at a time. No other transaction is allowed to perform read or write operations concurrently. This offers the highest level of isolation for records. However, it reduces concurrent access.


public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Override
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
Optional<Category> findById(Integer integer);
}

org.hibernate.SQL : select c1_0.id,c1_0.name,c1_0.version from category c1_0 where c1_0.id=? for no key update

When a SELECT statement includes “FOR NO KEY UPDATE,” it means that the database will acquire a shared lock on the selected rows, similar to “FOR SHARE.” However, it specifies that the lock should not include the primary key. This allows other transactions to acquire shared locks on the same rows for reading but without preventing them from acquiring exclusive locks for update or delete operations.

PESSIMISTIC_FORCE_INCREMENT

This is pretty similar to PESSIMISTIC_WRITE. It additionally increments a version attribute of a versioned entity. Most likely this lock mode is being used to provide optimistic locking.

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Category {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

private String name;

@Version
private Long version;
}
public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Override
@Lock(value = LockModeType.PESSIMISTIC_FORCE_INCREMENT)
Optional<Category> findById(Integer integer);
}

org.hibernate.SQL : select c1_0.id,c1_0.name,c1_0.version from category c1_0 where c1_0.id=? for no key update nowait

org.hibernate.SQL : update category set version=? where id=? and version=?

as you can see version column was incremented.

In summary, Pessimistic Locking is a great option to provide data integrity and consistency however it reduces concurrency. It might impact the performance of large-scale applications

Optimistic Locking in JPA

Before jumping in optimistic locking. I would like to show the best usage of an entity with version, soft delete, and common fields. Because If we want to enable Optimistic Locking, we must use @Version annotation. That’s why I would like to show the best usage way.

Some rules for the declaration of a version attribute must be followed.

  • Each entity class must have a version attribute
  • Each table must have a version column
  • The type of version must be numeric such as Integer, Short, or Long

BaseEntity

@Getter
@MappedSuperclass
public abstract class BaseEntity {

public static final Boolean DEFAULT_DELETED_VALUE = false;

@Id
@UuidGenerator
@Column(name = "ID", nullable = false, updatable = false)
private String id;

@Version
@Column(name = "VERSION", nullable = false)
private Long version;

@Column(name = "CREATED_AT", nullable = false, updatable = false)
private OffsetDateTime createdAt;

@Column(name = "UPDATED_AT")
private OffsetDateTime updatedAt;

@Column(name = "DELETED", nullable = false)
private Boolean deleted = DEFAULT_DELETED_VALUE;

@PrePersist
protected void prePersist() {
createdAt = OffsetDateTime.now();
}

@PreUpdate
protected void preUpdate() {
updatedAt = OffsetDateTime.now();
}

}

Actual Entity - Category

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "CATEGORY")
@Where(clause = "deleted is false")
@SQLDelete(sql = "UPDATE CATEGORY SET DELETED = TRUE WHERE ID=? and VERSION=?")
public class Category extends BaseEntity {

@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "NAME", nullable = false)
private String name;

}

We are going to cover two lock modes in Optimistic Locking.

OPTIMISTIC, OPTIMISITIC_FORCE_INCREMENT

OPTIMISTIC

JPA recommends using the OPTIMISTIC lock mode. When a transaction attempts to read a record, it aims to ensure that the version attribute has not been changed by another transaction. If the version attribute is not altered, it will be updated. However, if the version attributes do not match, it indicates a potential collision. In such a situation, the transaction can decide how to handle it either by throwing an exception or by retrying.

public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Override
@Lock(value = LockModeType.OPTIMISTIC)
Optional<Category> findById(Integer integer);
}

org.hibernate.SQL : select c1_0.id,c1_0.name,c1_0.version from category c1_0 where c1_0.id=?

select version as version_ from category where id=?

By the way, when an update operation is performed, the version attribute will be increment. But read operations will not be affected.

OPTIMISTIC_FORCE_INCREMENT

It is similar to OPTIMISTIC. It not only controls version collisions but also increments the version attribute. Even if the record does not change, it increments the version attribute to prevent potential collisions when performing a read operation by another transaction on the same record

This lock mode will increment version attribute for read and modification

public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Override
@Lock(value = LockModeType.OPTIMISTIC_FORCE_INCREMENT)
Optional<Category> findById(Integer integer);
}

org.hibernate.SQL : select c1_0.id,c1_0.name,c1_0.version from category c1_0 where c1_0.id=?

org.hibernate.SQL : update category set version=? where id=? and version=?

As you can see, after the read operation, the version was updated as 1.

Conclusion

In summary, Pessimistic Locking may be a better choice for data integrity and consistency; however, in large-scale applications, it can negatively impact performance by reducing concurrency. On the other hand, Optimistic Locking may be a better option for performance and concurrency; however, caution is needed in handling conflicts. It’s important to note that these choices may vary based on application requirements, performance expectations, and data integrity needs.

--

--