TroubleShooting_Concurrency issue
โ Concurrency Issue
Complex issue related to concurrent purchases of a popular product with limited stock. Multiple users attempted to buy the last few units of the product simultaneously, leading to race conditions where the stock count was not updated accurately, causing overselling.
๐ต Refactor 1. Database locking
prevent race conditions When a user initiates a purchase, a lock is placed on the productโs stock row until the transaction completes
โ๏ธ Database locking
- row-level locking: only that row lock, least restrictive lock
- table-level locking
- page-level locking
- pessimistic locking: no other transaction can modify until lock is released
- optimistic locking: multiple transactions can acess data, but checks for data changes
โ๏ธ Why pessimistic locking?
pessimistic locking
once a transaction reads a row, prevents other transactions from accessing until the first one completes.
- High likelihood of conflicts(row-level not adequateโ)
- Immediate locking
- Maintain data integrity
๐ Comparision with other locking mechanisms
- row-level
- row-level is suitable for low conflict rates. However, this issue seems hightly likely.
- table-level
- Overly restrictive. Multiple transactions need to access different rows in same table
- page-level
- Not as precise as row-level locking
- optimistic level
- suitable for low conflict rates. would lead to frequent transaction rollbacks and retries.
๐ข Outcome, Benefits
- eliminate chance of race conditions
- maintain data integrity
- sutability for high-conflic scenarios
โ๏ธ @Transactional
Before
1
2
3
4
5
6
7
8
9
10
11
12
//pessimistic database locking
public void optionStockChange(List<OptionQuantityDto> optionQuantityDtoList) {
for(OptionQuantityDto o: optionQuantityDtoList){
int optionId = o.getOptionId();
Options options= optionsRepository.findById(optionId)
.orElseThrow(()-> new NotFoundException("Cannot find option with ID"));
int orignialOptionStock= options.getStock();
int orderedStock= o.getQuantity();
options.setStock(orignialOptionStock - orderedStock);
optionsRepository.save(options);
}
}
After
1
2
3
4
5
6
7
8
9
10
11
12
13
//pessimistic database locking
@Transactional
public void optionStockChange(List<OptionQuantityDto> optionQuantityDtoList) {
for(OptionQuantityDto o: optionQuantityDtoList){
int optionId = o.getOptionId();
Options options= optionsRepository.findByIdWithLock(optionId) //repository change
.orElseThrow(()-> new NotFoundException("Cannot find option with ID"));
int orignialOptionStock= options.getStock();
int orderedStock= o.getQuantity();
options.setStock(orignialOptionStock - orderedStock);
optionsRepository.save(options);
}
}
โ๏ธ Repository
1
2
3
4
5
6
7
8
9
10
11
@Repository
public interface OptionsRepository extends JpaRepository<Options,Integer> {
//other methods...
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(
"SELECT o FROM Options o " +
"WHERE o.optionsId = :optionId "
)
Optional<Options> findByIdWithLock(int optionId);
}
โ๏ธ For save order, delete cart as well
In order service, optionId
was found from Option Repository
for saving order and deleting from cart as well. For these methods, implement @Transaction
and lock
as well.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Transactional
public void deleteFromCart(User user, List<OptionQuantityDto> optionQuantityDtoList) {
for(OptionQuantityDto o: optionQuantityDtoList){
Options options= options
Repository.findByIdWithLock(o.getOptionId())
.orElseThrow(()-> new NotFoundException("Cannot find option with ID"));
Cart cart= cartRepository.findByUserIdAndOptionId(user.getUserId(), options.getOptionsId())
.orElseThrow(() -> new NotFoundException("There is no product in cart with matching user and option."));
//finally delete cart
cartRepository.delete(cart);
}
}
@Transactional
public String saveOrder(User user, Integer optionId, String ordersNumber, LocalDate orderAt){
Options options= optionsRepository.findByIdWithLock(optionId)
.orElseThrow(()-> new NotFoundException("Cannot find option with ID"));
Orders orders= Orders.builder()
.user(user)
.options(options)
.ordersNumber(ordersNumber)
.ordersAt(orderAt)
.build();
ordersRepository.save(orders);
return "order saved successfully";
}
๐ต Refactor 2. Atomic Operations
ensure the stock count was decremented correctly without interference from other transactions.
1
2
3
4
5
6
7
synchronized (product) {
if (product.getStock() < requestedQuantity) {
throw new NotEnoughStockException("Requested quantity exceeds available stock.");
}
product.setStock(product.getStock() - requestedQuantity);
productRepository.save(product);
}
๐ต Refactor 3. Retry Mechanism
handle deadlock scenarios gracefully. If a transaction failed due to a deadlock, it was automatically retried.
1
2
3
4
5
6
7
8
9
10
int retries = 3;
while (retries > 0) {
try {
// Attempt transaction
break; // Break on success
} catch (DeadlockException e) {
retries--;
if (retries == 0) throw e; // Rethrow exception if out of retries
}
}