Два покупателя списали последний товар: условие внутри UPDATE

В приложении есть товар с остатком один. Каждый запрос сначала читает остаток, проверяет его в Python и затем записывает новое значение. Два параллельных запроса успевают прочитать единицу. Каждый считает покупку разрешённой, хотя товар нужен обоим.

Проверка в приложении и изменение строки выполняются в разные моменты. Между ними другой запрос может изменить остаток. Для простого списания проверку можно перенести в сам UPDATE: изменение допускается, только пока актуальное значение удовлетворяет условию.

Как изменить решение

UPDATE stock
SET available = available - 1
WHERE product_id = 42 AND available >= 1
RETURNING product_id, available;

Первый успешный запрос вернёт строку с новым остатком. Второй после ожидания конкурентного изменения не найдёт подходящую строку при Read Committed и вернёт пустой результат. Приложение должно трактовать отсутствие RETURNING как отказ списания, а не как успешную покупку.

Заказ и списание нужно зафиксировать одной транзакцией. Если создание заказа упало, откат должен вернуть остаток. Для заказа с несколькими товарами договоритесь о порядке блокировки строк: например, всегда по product_id. Это уменьшает вероятность взаимного ожидания при пересекающихся корзинах.

Что проверить на своих данных

Проверьте двумя соединениями: начните транзакцию в первом, выполните списание и не фиксируйте её; во втором выполните такую же команду. После COMMIT первого второе соединение должно получить ноль строк. Затем повторите опыт с ROLLBACK: второе списание сможет пройти.

Добавьте CHECK (available >= 0) как защиту целостности и NOT NULL для остатка. Ограничение не заменяет обработку отказа: оно помогает обнаружить другие пути записи, которые забыли условие. Для запроса нескольких единиц используйте available >= :quantity и проверяйте положительность quantity на входе. Отрицательное количество иначе превратит списание в пополнение.

Практика дальше

Списание становится полезным, когда обработчик API корректно возвращает отказ и сохраняет заказ. В курсе FastAPI вы собираете backend с SQLAlchemy, CRUD и тестами pytest. На последовательном проекте можно закрепить управление сессией и проверку ответов; база практики курса — SQLite.

Посмотреть программу и практику курса

Источник: Два покупателя списали последний товар — официальная документация.

Другие задачи по теме: PostgreSQL: запись, блокировки и планы запросов.

Продолжить на практике