Какие механизмы для управления конкурентным доступом и предотвращения «состояния гонки» существуют в Django

Ответ

В Django для управления конкурентным доступом к данным и предотвращения "состояния гонки" (race conditions) используются в основном механизмы, предоставляемые базой данных через Django ORM.

1. Пессимистические блокировки (select_for_update)

Этот метод блокирует строки в базе данных на время транзакции, не позволяя другим транзакциям изменять их. Это самый надежный способ предотвратить race conditions при операциях чтения-модификации-записи.

Принцип работы:

  1. Начать транзакцию.
  2. Выбрать и заблокировать строки с помощью select_for_update().
  3. Выполнить необходимые изменения.
  4. Завершить транзакцию, освободив блокировку.

Пример: безопасное увеличение счетчика

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.

Пример логики:

  1. Прочитать объект вместе с его версией: obj = Product.objects.get(id=1), read_version = obj.version.
  2. Выполнить изменения в памяти.
  3. При сохранении атомарно обновить объект, только если версия в БД не изменилась:
    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()