Ответ
В Django для управления конкурентным доступом к данным и предотвращения "состояния гонки" (race conditions) используются в основном механизмы, предоставляемые базой данных через Django ORM.
1. Пессимистические блокировки (select_for_update)
Этот метод блокирует строки в базе данных на время транзакции, не позволяя другим транзакциям изменять их. Это самый надежный способ предотвратить race conditions при операциях чтения-модификации-записи.
Принцип работы:
- Начать транзакцию.
- Выбрать и заблокировать строки с помощью
select_for_update(). - Выполнить необходимые изменения.
- Завершить транзакцию, освободив блокировку.
Пример: безопасное увеличение счетчика
from django.db import transaction
class Product(models.Model):
stock = models.IntegerField()
# Два запроса одновременно пытаются уменьшить stock
with transaction.atomic():
# Первый запрос, который выполнит эту строку, заблокирует ее
# Второй будет ждать, пока транзакция первого не завершится
product = Product.objects.select_for_update().get(id=1)
if product.stock > 0:
product.stock -= 1
product.save()
2. Атомарные транзакции (transaction.atomic)
Декоратор или контекстный менеджер, который гарантирует, что блок кода выполняется в рамках одной транзакции. Если в блоке происходит исключение, все изменения в БД откатываются. Это не блокировка сама по себе, а необходимое условие для корректной работы select_for_update.
3. Оптимистические блокировки (реализуются вручную)
Этот подход не блокирует строки в БД. Вместо этого перед обновлением записи проверяется, не изменил ли ее кто-то другой с момента чтения. Обычно это реализуется с помощью поля version или updated_at.
Пример логики:
- Прочитать объект вместе с его версией:
obj = Product.objects.get(id=1),read_version = obj.version. - Выполнить изменения в памяти.
- При сохранении атомарно обновить объект, только если версия в БД не изменилась:
updated_rows = Product.objects.filter(id=1, version=read_version).update(stock=new_stock, version=read_version + 1) if updated_rows == 0: # Конфликт! Кто-то изменил запись, нужно повторить операцию. raise ConcurrencyError("Failed to update product stock.")Оптимистические блокировки лучше подходят для систем с низкой вероятностью конфликтов, так как они не создают нагрузку на БД в виде блокировок.
4. Блокировки на уровне приложения (cache.lock)
Если ресурс не является строкой в БД (например, выполнение задачи по генерации отчета), можно использовать распределенные блокировки через кэш (Redis, Memcached).
from django.core.cache import cache
lock = cache.lock("report_generation_lock", timeout=60)
if lock.acquire(blocking=False):
try:
# Критическая секция: только один процесс может выполнять это одновременно
generate_report()
finally:
lock.release()