Skip to content

Commit 4dc848e

Browse files
committed
Retry version-less optimistic locking
1 parent 5ff1247 commit 4dc848e

File tree

4 files changed

+56
-140
lines changed

4 files changed

+56
-140
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.bookstore;
22

3-
import com.bookstore.service.InventoryService;
3+
import com.bookstore.service.BookstoreService;
44
import com.vladmihalcea.concurrent.aop.OptimisticConcurrencyControlAspect;
5+
import java.util.concurrent.ExecutorService;
6+
import java.util.concurrent.Executors;
7+
import java.util.concurrent.TimeUnit;
58
import org.springframework.boot.ApplicationRunner;
69
import org.springframework.boot.SpringApplication;
710
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -12,26 +15,38 @@
1215
@EnableAspectJAutoProxy
1316
public class MainApplication {
1417

15-
private final InventoryService inventoryService;
18+
private final BookstoreService bookstoreService;
1619

17-
public MainApplication(InventoryService inventoryService) {
18-
this.inventoryService = inventoryService;
20+
public MainApplication(BookstoreService bookstoreService) {
21+
this.bookstoreService = bookstoreService;
1922
}
2023

2124
public static void main(String[] args) {
2225
SpringApplication.run(MainApplication.class, args);
2326
}
2427

2528
@Bean
26-
public OptimisticConcurrencyControlAspect optimisticConcurrencyControlAspect() {
29+
public OptimisticConcurrencyControlAspect
30+
optimisticConcurrencyControlAspect() {
31+
2732
return new OptimisticConcurrencyControlAspect();
2833
}
29-
34+
3035
@Bean
3136
public ApplicationRunner init() {
3237
return args -> {
3338

34-
inventoryService.forceOptimisticLockException();
39+
ExecutorService executor = Executors.newFixedThreadPool(2);
40+
executor.execute(bookstoreService);
41+
// Thread.sleep(2000); -> adding a sleep here will break the transactions concurrency
42+
executor.execute(bookstoreService);
43+
44+
executor.shutdown();
45+
try {
46+
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
47+
} catch (InterruptedException ex) {
48+
Thread.currentThread().interrupt();
49+
}
3550
};
3651
}
3752
}
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,9 @@
11
package com.bookstore.repository;
22

33
import com.bookstore.entity.Inventory;
4-
import com.vladmihalcea.concurrent.Retry;
5-
import java.io.Serializable;
6-
import java.util.logging.Level;
7-
import java.util.logging.Logger;
8-
import javax.persistence.EntityManager;
9-
import javax.persistence.EntityManagerFactory;
10-
import javax.persistence.EntityTransaction;
11-
import javax.persistence.OptimisticLockException;
12-
import org.springframework.beans.factory.annotation.Autowired;
13-
import org.springframework.context.ApplicationContext;
14-
import org.springframework.context.annotation.Scope;
4+
import org.springframework.data.jpa.repository.JpaRepository;
155
import org.springframework.stereotype.Repository;
166

177
@Repository
18-
@Scope("prototype")
19-
public class InventoryRepository implements Serializable, Runnable {
20-
21-
private final int quantity;
22-
private final int delay;
23-
24-
@Autowired
25-
private ApplicationContext applicationContext;
26-
27-
private static final Logger logger = Logger.getLogger(InventoryRepository.class.getName());
28-
29-
public InventoryRepository(int quantity, int delay) {
30-
this.quantity = quantity;
31-
this.delay = delay;
32-
}
33-
34-
@Override
35-
@Retry(times = 10, on = OptimisticLockException.class)
36-
public void run() {
37-
38-
EntityManagerFactory entityManagerFactory
39-
= applicationContext.getBean(EntityManagerFactory.class);
40-
41-
EntityManager entityManager = entityManagerFactory.createEntityManager();
42-
43-
EntityTransaction entityTransaction = entityManager.getTransaction();
44-
45-
try {
46-
entityTransaction.begin();
47-
48-
Inventory inventory = entityManager.find(Inventory.class, 1L);
49-
50-
if ((inventory.getQuantity() - quantity) >= 0) {
51-
inventory.setQuantity(inventory.getQuantity() - quantity);
52-
}
53-
54-
try {
55-
Thread.sleep(delay);
56-
} catch (InterruptedException ex) {
57-
Thread.currentThread().interrupt();
58-
}
59-
60-
entityTransaction.commit();
61-
62-
logger.info("Transaction committed ...");
63-
} catch (RuntimeException e) {
64-
65-
if (is(e, OptimisticLockException.class)) {
66-
logger.log(Level.INFO, "OptimisticLockException has occured ...");
67-
logger.log(Level.INFO, "Retry mechanism enter into the scene ...");
68-
}
69-
70-
if (entityTransaction != null && entityTransaction.isActive()) {
71-
entityTransaction.rollback();
72-
}
73-
74-
throw (e);
75-
}
76-
}
77-
78-
public static <T extends Throwable> boolean is(Throwable exception, Class<T> type) {
79-
Throwable unwrappedException = exception;
80-
81-
while (unwrappedException != null) {
82-
if (type.isInstance(unwrappedException)) {
83-
return true;
84-
}
85-
86-
unwrappedException = unwrappedException.getCause();
87-
}
88-
89-
return false;
90-
}
8+
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
919
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.bookstore.service;
2+
3+
import com.vladmihalcea.concurrent.Retry;
4+
import org.springframework.dao.OptimisticLockingFailureException;
5+
import org.springframework.stereotype.Service;
6+
7+
@Service
8+
public class BookstoreService implements Runnable {
9+
10+
private final InventoryService inventoryService;
11+
12+
public BookstoreService(InventoryService inventoryService) {
13+
this.inventoryService = inventoryService;
14+
}
15+
16+
17+
@Override
18+
@Retry(times = 10, on = OptimisticLockingFailureException.class)
19+
public void run() {
20+
inventoryService.updateQuantity();
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,23 @@
11
package com.bookstore.service;
22

3+
import com.bookstore.entity.Inventory;
34
import com.bookstore.repository.InventoryRepository;
4-
import java.util.concurrent.ExecutorService;
5-
import java.util.concurrent.Executors;
6-
import java.util.concurrent.TimeUnit;
7-
import java.util.logging.Logger;
8-
import org.springframework.context.ApplicationContext;
95
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
107

118
@Service
129
public class InventoryService {
1310

14-
private static final Logger logger = Logger.getLogger(InventoryService.class.getName());
11+
private final InventoryRepository inventoryRepository;
1512

16-
private final ApplicationContext applicationContext;
17-
18-
public InventoryService(ApplicationContext applicationContext) {
19-
this.applicationContext = applicationContext;
13+
public InventoryService(InventoryRepository inventoryRepository) {
14+
this.inventoryRepository = inventoryRepository;
2015
}
16+
17+
@Transactional
18+
public void updateQuantity() {
2119

22-
private static final ExecutorService executor
23-
= Executors.newFixedThreadPool(2);
24-
25-
public void forceOptimisticLockException() {
26-
27-
// Start the first transaction
28-
// This transaction will decrease the inventory quantity by two units
29-
// Right before committing, this transaction is put on sleep for 5 seconds
30-
logger.info("Starting first transaction ...");
31-
InventoryRepository thread1
32-
= applicationContext.getBean(InventoryRepository.class, 2, 5000);
33-
executor.execute(thread1);
34-
35-
// Give some time to the first transaction to start
36-
try {
37-
Thread.sleep(1000);
38-
} catch (InterruptedException ex) {
39-
Thread.currentThread().interrupt();
40-
}
41-
42-
// Start the second transaction
43-
// This transaction will decrease the inventory quantity by one unit (from 10 to 9)
44-
// This transaction will execute and commit without waiting
45-
// While this transaction is running, the first one sleeps, so when the
46-
// first transaction wakes up it will cause an OptimisticLockException since
47-
// this transaction will decrease the quantity from 10 to 9
48-
// But, at retry, it will work as expected since the first transaction will try to decrease the
49-
// quantity from 9 to 7
50-
logger.info("Starting second transaction ...");
51-
InventoryRepository thread2
52-
= applicationContext.getBean(InventoryRepository.class, 1, 1);
53-
executor.execute(thread2);
54-
55-
executor.shutdown();
56-
try {
57-
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
58-
} catch (InterruptedException ex) {
59-
Thread.currentThread().interrupt();
60-
}
20+
Inventory inventory = inventoryRepository.findById(1L).orElseThrow();
21+
inventory.setQuantity(inventory.getQuantity() - 2);
6122
}
6223
}

0 commit comments

Comments
 (0)