avatarVinotech

Summary

The provided content explains how to implement pessimistic locking in a Spring Boot application using JPA to ensure data consistency and integrity during concurrent transactions.

Abstract

Pessimistic locking is a database transaction control mechanism that prevents conflicts by locking records before any modifications are made. In the context of a Spring Boot application, pessimistic locking can be implemented using JPA's LockModeType to acquire write or read locks on entities. The article details the steps to set up pessimistic locking, including adding Maven dependencies, defining the Account entity with a version field for optimistic locking, creating an AccountRepository with a pessimistic write lock method, and implementing an AccountService with a transactional withdrawal method. It also demonstrates how to test simultaneous requests using threads to simulate concurrent transactions and ensure that only one transaction can proceed at a time, maintaining data integrity. The example scenario involves an Account entity where two users attempt to withdraw money from the same account, with the expectation that only one withdrawal will succeed if the balance is insufficient after the first transaction.

Opinions

  • The author emphasizes the importance of pessimistic locking for data consistency in concurrent environments.
  • Pessimistic write locks are recommended for write operations to prevent other transactions from modifying the locked resource.
  • Pessimistic read locks are suggested for scenarios where data consistency is critical, and read operations are frequent.
  • The article provides a practical example to illustrate the concept, suggesting that the author values hands-on learning and demonstration.
  • The author encourages readers to engage with the content by asking them to consider giving it claps and sharing it if found useful, indicating a desire for community feedback and recognition.
  • The article concludes with additional resources for readers who wish to explore related topics, showing the author's commitment to comprehensive learning and the dissemination of knowledge.

Pessimistic Locking in JPA with Spring Boot

Understanding Pessimistic Locking

Before diving into Spring Boot implementations, let’s establish a solid understanding of pessimistic locking and its significance in database transactions. Pessimistic locking is a concurrency control mechanism where a lock is acquired on a database record or resource before performing any modifications. This lock prevents other transactions from accessing or modifying the locked resource until the lock is released, thereby ensuring data consistency and preventing conflicts in concurrent environments.

Pessimistic locking ensures that a database row is locked for the duration of a transaction to prevent other transactions from modifying it. When two requests try to update the same record simultaneously, one will wait until the other completes.

Implementing Pessimistic Locking in Spring Boot

Spring Boot provides support for implementing pessimistic locking through its integration with JPA (Java Persistence API) and the underlying database. Let’s explore how to implement pessimistic locking in Spring Boot using various strategies and techniques.

1. Pessimistic Write Lock

Pessimistic write lock, also known as “SELECT … FOR UPDATE,” is a common approach to acquiring a lock on database records for write operations. In Spring Boot, you can leverage JPA’s LockModeType to acquire a pessimistic write lock on entities.

2. Pessimistic Read Lock

Pessimistic read lock allows multiple transactions to read the same data concurrently while preventing any transaction from modifying the locked data. This can be useful for scenarios where data consistency is critical, but read operations are frequent.

Example Scenario

Let’s consider an Account entity where two users try to withdraw money from the same account at the same time. We will use Pessimistic Locking to ensure that only one user can perform the withdrawal at a time.

1. Maven Dependencies

Ensure you have the following dependencies in your pom.xml:

<dependencies>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- H2 Database for testing -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2. Account Entity

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.Lock;

@Entity
public class Account {

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

    private String accountNumber;
    private Double balance;

    @Version
    private Integer version;

    // Getters and Setters
}

3. AccountRepository

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;

import jakarta.persistence.LockModeType;

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Account findByAccountNumber(String accountNumber);
}

4. AccountService

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void withdraw(String accountNumber, Double amount) {
        Account account = accountRepository.findByAccountNumber(accountNumber);
        
        if (account.getBalance() >= amount) {
            account.setBalance(account.getBalance() - amount);
            accountRepository.save(account);
        } else {
            throw new RuntimeException("Insufficient balance");
        }
    }
}

5. AccountController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/accounts")
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PostMapping("/withdraw")
    public String withdraw(@RequestBody WithdrawRequest request) {
        accountService.withdraw(request.getAccountNumber(), request.getAmount());
        return "Withdrawal successful";
    }
}

class WithdrawRequest {
    private String accountNumber;
    private Double amount;

    // Getters and Setters
}

6. Testing Simultaneous Requests

To test this, you can create two threads simulating simultaneous requests.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class TestRunner implements CommandLineRunner {

    @Autowired
    private AccountService accountService;

    @Override
    public void run(String... args) throws Exception {
        // Create an account with a balance
        Account account = new Account();
        account.setAccountNumber("12345");
        account.setBalance(1000.0);
        accountRepository.save(account);

        // Simulate two users trying to withdraw money at the same time
        Runnable task1 = () -> accountService.withdraw("12345", 800.0);
        Runnable task2 = () -> accountService.withdraw("12345", 300.0);

        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

7. Sample Input and Output

Initial State:

  • Account 12345 with Balance = 1000.0

Simultaneous Requests:

  • Thread 1: Withdraw 800.0
  • Thread 2: Withdraw 300.0

Expected Output:

  • Thread 1 successfully withdraws 800.0, leaving a balance of 200.0.
  • Thread 2 fails with “Insufficient balance” because the account balance was reduced to 200.0 by Thread 1.

Console Output:

Withdrawal successful (Thread 1)
Exception in thread "Thread-2" java.lang.RuntimeException: Insufficient balance

Explanation

When both threads attempt to withdraw from the account, the first one that acquires the lock (Thread 1) processes its transaction. Thread 2 will be blocked until Thread 1 completes. When Thread 2 resumes, it finds that the balance is insufficient and throws an exception.

This example shows how Pessimistic Locking ensures data integrity by preventing simultaneous modifications to the same record.

👏 If you found my articles useful, please consider giving it claps and sharing it with your friends and colleagues.

Pessimistic Locking
Pessimistic Spring Boot
Spring Data Jpa
Database Transaction
Concurrency Control
Recommended from ReadMedium