Проблема Race Condition в Django и её решение с помощью select_for_update
Автор: Javohir Abdullayev · · Django

Введение: Неожиданные потери баланса и состояние гонки (Race Condition)
Каждый backend-разработчик за свою карьеру хотя бы раз сталкивается со скрытыми и трудноуловимыми ошибками. Одной из самых опасных и сложных в обнаружении является Race Condition (состояние гонки). В небольших проектах и локальной среде кажется, что всё работает безупречно. Однако когда на сайт одновременно заходят сотни пользователей и обращаются к одному и тому же ресурсу (например, к балансу счёта или покупке билетов), возникают серьёзные финансовые и логические ошибки.
В этой статье мы разберём классическую проблему, встречающуюся в реальных системах электронной коммерции и платежей, её причины, а также то, как правильно решить её с помощью select_for_update() в Django ORM.
Проблема в реальном проекте (Case Study)
Представьте, что вы разработали систему управления внутренним балансом кошелька пользователей. У каждого пользователя на счету 100 000 сум. Пользователь одновременно из двух разных вкладок браузера или через Telegram-бота нажимает кнопку покупки товара стоимостью 80 000 сум два раза подряд.
Обычно начинающие разработчики пишут код следующим образом:
def withdraw_money(user_id, amount):\n wallet = Wallet.objects.get(user_id=user_id)\n if wallet.balance >= amount:\n wallet.balance -= amount\n wallet.save()\n return True\n return FalseПочему этот код опасен?
Если два запроса приходят на сервер практически одновременно (с разницей в несколько миллисекунд), оба процесса считывают из базы данных баланс 100 000 сум. Оба запроса считают условие 100 000 >= 80 000 истинным, вычитают 80 000 из баланса и сохраняют в базу значение 20 000. В результате пользователь получает товары на общую сумму 160 000 сум, но с его баланса списывается всего 80 000 сум! Компания несёт убыток в размере 60 000 сум.
Решение: Транзакции в базе данных и select_for_update
Самый надёжный способ решить эту проблему — блокировка строк на уровне базы данных (row-level locking). Django позволяет очень удобно реализовать это с помощью transaction.atomic() и метода select_for_update().
Пример исправленного кода
from django.db import transaction\n\ndef safe_withdraw_money(user_id, amount):\n with transaction.atomic():\n wallet = Wallet.objects.select_for_update().get(user_id=user_id)\n \n if wallet.balance >= amount:\n wallet.balance -= amount\n wallet.save()\n return True\n else:\n return FalseКак это работает?
- transaction.atomic(): Оборачивает все операции в единую транзакцию. Если на каком-либо этапе произойдёт ошибка, все изменения будут отменены (rollback).
- select_for_update(): Отправляет на уровне SQL запрос
SELECT ... FOR UPDATE. Это накладывает блокировку (lock) именно на эту запись в базе данных PostgreSQL или MySQL. - Когда поступает второй запрос, он ожидает завершения (commit) первой транзакции. После завершения первой транзакции второй запрос считывает уже обновлённый баланс (20 000 сум) и отклоняется из-за недостаточности средств.
Дополнительная защита с помощью F-выражений
Для простых вычислений существует ещё один отличный инструмент — F-выражения Django F(). Они обновляют значение напрямую в базе данных, а не в памяти приложения:
from django.db.models import F\n\nWallet.objects.filter(id=wallet.id, balance__gte=amount).update(balance=F('balance') - amount)Такой подход выполняет проверку и списание одним SQL-запросом и работает исключительно быстро для простых операций.
Важные рекомендации и меры предосторожности
- Риск взаимной блокировки (Deadlock): При одновременной блокировке нескольких объектов всегда блокируйте их в одинаковом порядке (например, сортируя по ID).
- Использование параметра nowait: Если вы не хотите долгого ожидания в очереди, можно указать
select_for_update(nowait=True), чтобы немедленно вернуть ошибку при наличии блокировки. - Используйте только там, где это необходимо: Блокировка при каждом обычном запросе на чтение снижает производительность системы, поэтому применяйте её только в финансовых и критически важных транзакциях.
Заключение
Ошибки Race Condition — одна из самых больших головных болей при росте числа пользователей проекта. Правильно применяя транзакции и метод select_for_update(), предоставляемый Django ORM, вы можете на 100% гарантировать финансовую безопасность и целостность данных вашего проекта.
Теги: #Django #Backend #PostgreSQL #Ma'lumotlar bazasi