«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > Как бороться с условиями гонки с помощью Java и PostgreSQL

Как бороться с условиями гонки с помощью Java и PostgreSQL

Опубликовано 1 августа 2024 г.
Просматривать:559

How to deal with race conditions using Java and PostgreSQL

Использование блокировки для управления параллелизмом базы данных

Представьте, что вы работаете над системой электронной коммерции, и тысячи людей одновременно пытаются купить последний оставшийся продукт. Однако многие из них смогли перейти к кассе и завершить заказ. Когда вы проверяете свои запасы, у вас есть продукт с отрицательным количеством. Как это стало возможным и как вы можете это решить?

Давайте кодировать! Первое, о чем вы можете подумать, — это проверить наличие товара перед оформлением заказа. Может быть, что-то вроде этого:

public void validateAndDecreaseSolution(long productId, int quantity {
    Optional stockByProductId = 
 stockRepository.findStockByProductId(productId);

    int stock = stockByProductId.orElseThrow().getStock();
    int possibleStock = stock - quantity;

    if (stock 



Вы можете использовать эту проверку, но когда мы говорим о сотнях, тысячах, миллионах или даже десятках запросов в секунду, этой проверки будет недостаточно. Когда 10 запросов достигнут этого фрагмента кода одновременно и база данных вернет одно и то же значение для stockByProductId, ваш код сломается. Вам нужен способ заблокировать другие запросы, пока мы проводим эту проверку.

Первое решение - ДЛЯ ОБНОВЛЕНИЯ

Добавьте оператор блокировки в свой SELECT. В этом примере я сделал это, используя FOR UPDATE с Spring Data. Как сказано в документации PostgreSQL

FOR UPDATE приводит к тому, что строки, полученные инструкцией SELECT, блокируются, как если бы они были обновлены. Это предотвращает их изменение или удаление другими транзакциями до тех пор, пока текущая транзакция не завершится.

@Query(value = "SELECT * FROM stocks s WHERE s.product_id = ?1 FOR UPDATE", nativeQuery = true)
Optional findStockByProductIdWithLock(Long productId);
public void validateAndDecreaseSolution1(long productId, int quantity) {
    Optional stockByProductId = stockRepository.findStockByProductIdWithLock(productId);

    // ... validate

    stockRepository.decreaseStock(productId, quantity);
}

Все запросы к таблице акций с использованием идентификатора продукта будут ждать завершения фактической транзакции. Цель здесь — убедиться, что вы получаете последнюю обновленную стоимость акций.

Второе решение — pg_advisory_xact_lock

Это решение похоже на предыдущее, но вы можете выбрать ключ блокировки. Мы заблокируем всю транзакцию до тех пор, пока не завершим всю обработку проверки и уменьшения запасов.

public void acquireLockAndDecreaseSolution2(long productId, int quantity) {
    Query nativeQuery = entityManager.createNativeQuery("select pg_advisory_xact_lock(:lockId)");
    nativeQuery.setParameter("lockId", productId);
    nativeQuery.getSingleResult();

    Optional stockByProductId = stockRepository.findStockByProductId(productId);

    // check stock and throws exception if it is necessary

    stockRepository.decreaseStock(productId, quantity);
}

Следующий запрос будет взаимодействовать только с продуктом с тем же идентификатором после завершения этой транзакции.

Третье решение - предложение WHERE

В этом случае мы не будем блокировать нашу строку или транзакцию. Давайте разрешим этой транзакции продолжаться до оператора обновления. Обратите внимание на последнее условие: запас > 0. Это не позволит нашему запасу быть меньше нуля. Таким образом, если два человека попытаются купить одновременно, один из них получит сообщение об ошибке, поскольку наша база данных не допускает акции

@Transactional
@Modifying
@Query(nativeQuery = true, value = "UPDATE stocks SET stock = stock - :quantity WHERE product_id = :productId AND stock > 0")
int decreaseStockWhereQuantityGreaterThanZero(@Param("productId") Long productId, @Param("quantity") int quantity);

Заключение

Первое и второе решения используют в качестве стратегии пессимистическую блокировку. Третий – оптимистическая блокировка. Стратегия пессимистической блокировки используется, когда вы хотите ограничить доступ к ресурсу при выполнении любой задачи, связанной с этим ресурсом. Целевой ресурс будет заблокирован для любого другого доступа, пока вы не завершите процесс. Будьте осторожны с взаимоблокировками!

При оптимистической блокировке вы можете выполнять различные запросы к одному и тому же ресурсу без какой-либо блокировки. Он используется, когда конфликты маловероятны. Обычно у вас есть версия, связанная с вашей строкой, и когда вы обновляете эту строку, база данных сравнивает версию вашей строки с версией строки в базе данных. Если оба равны, изменение будет успешным. Если нет, вам придется повторить попытку. Как видите, в этой статье я не использую ни одной строки версии, но мое третье решение не блокирует никакие запросы и контролирует параллелизм, используя условие stock > 0.

Если вы хотите увидеть полный код, вы можете проверить его на моем GitHub.

Существует много других стратегий для реализации пессимистической и оптимистичной блокировки, например, вы можете поискать больше о FOR UPDATE With SKIP LOCKED.

Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/ramoncunha/how-to-deal-with-race-conditions-using-java-and-postgresql-4jk6?1. Если есть какие-либо нарушения, пожалуйста, свяжитесь с Study_golang@163. .com, чтобы удалить его
Последний учебник Более>

Изучайте китайский

Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

Copyright© 2022 湘ICP备2022001581号-3