Optimistic vs Pessimistic Locking

Overview

When an app is deployed on more than one server how to you ensure that 2 threads dont modify the same record in db? If the operation was performed on a single JVM you could look at locking but since there are many jvm the locking has to be done at database level.

Github: https://github.com/gitorko/project67

Locking

Lets say we have a ticket booking service with a table holding all the free tickets and multiple servers running our app. How do we ensure that 2 users cant book the same seat? Since the app runs on different JVM we cant syncronize or use jvm locks.

Hibernate provides two approaches to handle concurrency at database level:

  1. Pessimistic Locking - The lock is now applied by the database at row level or table level. If the lock is a WRITE lock it prevents other threads from modifying the data. eg: SELECT * from TABLE where id = 1 for update;
  2. Optimistic Locking - A version field is introduced to the database table, The JPA ensures that version check is done before saving data, if the version has changed the update will throw Error. Scalability is high with this approach.

Pessimistic locking

Three Pessimistic LockModeTypes are supported in JPA.

  1. PESSIMISTIC_READ - Rows are locked and can be read by other transactions, but they cannot be deleted or modified. PESSIMISTIC_READ guarantees repeatable reads.
  2. PESSIMISTIC_WRITE - Rows are locked and cannot be read, modified or deleted by other transactions. For PESSIMISTIC_WRITE no phantom reads can occur and access to data must be serialized.
  3. PESSIMISTIC_FORCE_INCREMENT - Rows are locked and cannot be modified or deleted. For versioned entities, their version number is incremented as soon as the query executes.
  1package com.demo.project67.pessimistic;
  2
  3import java.util.Date;
  4import java.util.concurrent.ExecutorService;
  5import java.util.concurrent.Executors;
  6import java.util.concurrent.TimeUnit;
  7import javax.persistence.Entity;
  8import javax.persistence.GeneratedValue;
  9import javax.persistence.GenerationType;
 10import javax.persistence.Id;
 11import javax.persistence.LockModeType;
 12import javax.persistence.QueryHint;
 13import javax.persistence.Temporal;
 14import javax.persistence.TemporalType;
 15import javax.transaction.Transactional;
 16
 17import lombok.AllArgsConstructor;
 18import lombok.Builder;
 19import lombok.Data;
 20import lombok.NoArgsConstructor;
 21import net.bytebuddy.implementation.bytecode.Throw;
 22import org.springframework.beans.factory.annotation.Autowired;
 23import org.springframework.boot.CommandLineRunner;
 24import org.springframework.boot.SpringApplication;
 25import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 26import org.springframework.context.annotation.Bean;
 27import org.springframework.context.annotation.ComponentScan;
 28import org.springframework.context.annotation.Configuration;
 29import org.springframework.data.jpa.repository.JpaRepository;
 30import org.springframework.data.jpa.repository.Lock;
 31import org.springframework.data.jpa.repository.QueryHints;
 32import org.springframework.stereotype.Component;
 33import org.springframework.stereotype.Repository;
 34
 35@Configuration
 36@EnableAutoConfiguration
 37@ComponentScan(basePackages = "com.demo.project67.pessimistic")
 38public class PessimisticMain {
 39
 40    @Autowired
 41    MyService myService;
 42
 43    ExecutorService pool = Executors.newCachedThreadPool();
 44
 45    public static void main(String[] args) {
 46        SpringApplication.run(PessimisticMain.class, args);
 47    }
 48
 49    @Bean
 50    public CommandLineRunner commandLineRunner() {
 51        return args -> {
 52            myService.seedData();
 53            pool.submit(() -> {
 54                Boolean status = myService.bookSeat(1, "Joe");
 55                System.out.println("Booking for Joe success: "+ status);
 56            });
 57            pool.submit(() -> {
 58                Boolean status = myService.bookSeat(1, "Jack");
 59                System.out.println("Booking for Jack success: "+ status);
 60            });
 61            pool.shutdown();
 62            pool.awaitTermination(60, TimeUnit.SECONDS);
 63            myService.showData();
 64        };
 65    }
 66}
 67
 68@Entity
 69@Data
 70@Builder
 71@AllArgsConstructor
 72@NoArgsConstructor
 73class Ticket {
 74    @Id
 75    @GeneratedValue(strategy = GenerationType.AUTO)
 76    private Long id;
 77    private Integer seatNumber;
 78    @Temporal(TemporalType.DATE)
 79    private Date onDay;
 80    private String bookedBy;
 81}
 82
 83@Repository
 84interface TicketRepository extends JpaRepository<Ticket, Long> {
 85
 86    @Lock(LockModeType.PESSIMISTIC_WRITE)
 87    @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
 88    Ticket findBySeatNumberAndOnDay(Integer seatNumber, Date onDay);
 89
 90}
 91
 92@Component
 93class MyService {
 94    @Autowired
 95    TicketRepository ticketRepository;
 96
 97    @Transactional
 98    public Boolean bookSeat(Integer seatNumber, String personName) {
 99        try {
100            System.out.println("Booking seat: " + seatNumber + " By: " + personName);
101            Ticket seat = ticketRepository.findBySeatNumberAndOnDay(seatNumber, new Date());
102            if (seat.getBookedBy() == null) {
103                seat.setBookedBy(personName);
104                ticketRepository.save(seat);
105                return true;
106            } else {
107                return false;
108            }
109        } catch (Exception ex) {
110            System.out.println(ex.getMessage());
111        }
112        return false;
113    }
114
115    public void showData() {
116        ticketRepository.findAll().forEach(e -> {
117            System.out.println(e);
118        });
119    }
120
121    public void seedData() {
122        ticketRepository.deleteAll();
123        for (int i = 1; i <= 3; i++) {
124            ticketRepository.save(Ticket.builder().seatNumber(i).onDay(new Date()).build());
125        }
126    }
127}

Run the code and you will see that only one person is able to book the ticket. Notice the 'for update' in the sql query that is fired which will lock the row.

 1Booking seat: 1 By: Joe
 2Booking seat: 1 By: Jack
 3Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_ from ticket ticket0_ where ticket0_.seat_number=? and ticket0_.on_day=? for update
 4Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_ from ticket ticket0_ where ticket0_.seat_number=? and ticket0_.on_day=? for update
 5Hibernate: update ticket set booked_by=?, on_day=?, seat_number=? where id=?
 6Booking for Jack success: true
 7Booking for Joe success: false
 8Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_ from ticket ticket0_
 9Ticket(id=1, seatNumber=1, onDay=2020-08-17, bookedBy=Jack)
10Ticket(id=2, seatNumber=2, onDay=2020-08-17, bookedBy=null)
11Ticket(id=3, seatNumber=3, onDay=2020-08-17, bookedBy=null)

Optimistic locking

  1package com.demo.project67.optimistic;
  2
  3import java.util.Date;
  4import java.util.concurrent.ExecutorService;
  5import java.util.concurrent.Executors;
  6import java.util.concurrent.TimeUnit;
  7import javax.persistence.Entity;
  8import javax.persistence.GeneratedValue;
  9import javax.persistence.GenerationType;
 10import javax.persistence.Id;
 11import javax.persistence.Temporal;
 12import javax.persistence.TemporalType;
 13import javax.persistence.Version;
 14import javax.transaction.Transactional;
 15
 16import lombok.AllArgsConstructor;
 17import lombok.Builder;
 18import lombok.Data;
 19import lombok.NoArgsConstructor;
 20import org.springframework.beans.factory.annotation.Autowired;
 21import org.springframework.boot.CommandLineRunner;
 22import org.springframework.boot.SpringApplication;
 23import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 24import org.springframework.context.annotation.Bean;
 25import org.springframework.context.annotation.ComponentScan;
 26import org.springframework.context.annotation.Configuration;
 27import org.springframework.data.jpa.repository.JpaRepository;
 28import org.springframework.stereotype.Component;
 29import org.springframework.stereotype.Repository;
 30
 31@Configuration
 32@EnableAutoConfiguration
 33@ComponentScan(basePackages = "com.demo.project67.optimistic")
 34public class OptimisticMain {
 35
 36    @Autowired
 37    MyService myService;
 38
 39    ExecutorService pool = Executors.newCachedThreadPool();
 40
 41    public static void main(String[] args) {
 42        SpringApplication.run(OptimisticMain.class, args);
 43    }
 44
 45    @Bean
 46    public CommandLineRunner commandLineRunner() {
 47        return args -> {
 48            myService.seedData();
 49            pool.submit(() -> {
 50                Boolean status = myService.bookSeat(1, "Joe");
 51                System.out.println("Booking for Joe success: "+ status);
 52            });
 53            pool.submit(() -> {
 54                Boolean status = myService.bookSeat(1, "Jack");
 55                System.out.println("Booking for Jack success: "+ status);
 56            });
 57            pool.shutdown();
 58            pool.awaitTermination(60, TimeUnit.SECONDS);
 59            myService.showData();
 60        };
 61    }
 62}
 63
 64@Entity
 65@Data
 66@Builder
 67@AllArgsConstructor
 68@NoArgsConstructor
 69class Ticket {
 70    @Id
 71    @GeneratedValue(strategy = GenerationType.AUTO)
 72    private Long id;
 73    private Integer seatNumber;
 74    @Temporal(TemporalType.DATE)
 75    private Date onDay;
 76    private String bookedBy;
 77    @Version
 78    private int version;
 79}
 80
 81@Repository
 82interface TicketRepository extends JpaRepository<Ticket, Long> {
 83    @Transactional
 84    Ticket findBySeatNumberAndOnDay(Integer seatNumber, Date onDay);
 85}
 86
 87@Component
 88class MyService {
 89    @Autowired
 90    TicketRepository ticketRepository;
 91
 92    public Boolean bookSeat(Integer seatNumber, String personName) {
 93        try {
 94            System.out.println("Booking seat: " + seatNumber + " By: " + personName);
 95            Ticket seat = ticketRepository.findBySeatNumberAndOnDay(seatNumber, new Date());
 96            if (seat.getBookedBy() == null) {
 97                seat.setBookedBy(personName);
 98                ticketRepository.save(seat);
 99                return true;
100            } else {
101                return false;
102            }
103        } catch (Exception ex) {
104            System.out.println(ex.getMessage());
105        }
106        return false;
107    }
108
109    public void showData() {
110        ticketRepository.findAll().forEach(e -> {
111            System.out.println(e);
112        });
113    }
114
115    public void seedData() {
116        ticketRepository.deleteAll();
117        for (int i = 1; i <= 3; i++) {
118            ticketRepository.save(Ticket.builder().seatNumber(i).onDay(new Date()).build());
119        }
120    }
121}

Run the code and you will see that only one person is able to book the ticket. Notice the exception of 'StaleObjectStateException' thrown.

 1Booking seat: 1 By: Joe
 2Booking seat: 1 By: Jack
 3Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_, ticket0_.version as version5_0_ from ticket ticket0_ where ticket0_.seat_number=? and ticket0_.on_day=?
 4Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_, ticket0_.version as version5_0_ from ticket ticket0_ where ticket0_.seat_number=? and ticket0_.on_day=?
 5Hibernate: select ticket0_.id as id1_0_0_, ticket0_.booked_by as booked_b2_0_0_, ticket0_.on_day as on_day3_0_0_, ticket0_.seat_number as seat_num4_0_0_, ticket0_.version as version5_0_0_ from ticket ticket0_ where ticket0_.id=?
 6Hibernate: select ticket0_.id as id1_0_0_, ticket0_.booked_by as booked_b2_0_0_, ticket0_.on_day as on_day3_0_0_, ticket0_.seat_number as seat_num4_0_0_, ticket0_.version as version5_0_0_ from ticket ticket0_ where ticket0_.id=?
 7Hibernate: update ticket set booked_by=?, on_day=?, seat_number=?, version=? where id=? and version=?
 8Hibernate: update ticket set booked_by=?, on_day=?, seat_number=?, version=? where id=? and version=?
 9Booking for Joe success: true
10Hibernate: select ticket0_.id as id1_0_0_, ticket0_.booked_by as booked_b2_0_0_, ticket0_.on_day as on_day3_0_0_, ticket0_.seat_number as seat_num4_0_0_, ticket0_.version as version5_0_0_ from ticket ticket0_ where ticket0_.id=?
11Object of class [com.demo.project67.optimistic.Ticket] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.demo.project67.optimistic.Ticket#1]
12Booking for Jack success: false
13Hibernate: select ticket0_.id as id1_0_, ticket0_.booked_by as booked_b2_0_, ticket0_.on_day as on_day3_0_, ticket0_.seat_number as seat_num4_0_, ticket0_.version as version5_0_ from ticket ticket0_
14Ticket(id=1, seatNumber=1, onDay=2020-08-17, bookedBy=Joe, version=1)
15Ticket(id=2, seatNumber=2, onDay=2020-08-17, bookedBy=null, version=0)
16Ticket(id=3, seatNumber=3, onDay=2020-08-17, bookedBy=null, version=0)
1spring:
2  main:
3    web-application-type: none
4  jpa:
5    hibernate.ddl-auto: create-drop
6    show-sql: true

References

Spring Data JPA : https://spring.io/projects/spring-data-jpa

comments powered by Disqus