Post

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.

  1. High likelihood of conflicts(row-level not adequateโŒ)
  2. Immediate locking
  3. 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
    }
}
This post is licensed under CC BY 4.0 by the author.