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:
- 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;
- 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.
- PESSIMISTIC_READ - Rows are locked and can be read by other transactions, but they cannot be deleted or modified. PESSIMISTIC_READ guarantees repeatable reads.
- 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.
- 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