Какие способы кроме уровня изоляции помогают при решении race condition?
Что проверяют
Понимаете ли вы, где возникает гонка в бэкенде на Python: чаще не между потоками, а между запросами в разных процессах, которые читают и пишут одну строку в базе. Следом обычно просят решить задачу про списание баланса или бронирование последнего места.
Как отвечать
Сначала разделите уровни. threading.Lock и asyncio.Lock защищают только внутри одного процесса, а GIL не делает counter += 1 атомарным. Под Gunicorn с несколькими воркерами и подами синхронизацию переносят туда, где лежат данные.
Способы на уровне базы:
- атомарное обновление:
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100и проверка числа изменённых строк. В Django этоF()вupdate(). Вычисление в Python иsave()теряют чужие изменения; - пессимистическая блокировка:
SELECT ... FOR UPDATEвнутри транзакции, в Djangoselect_for_update(), в SQLAlchemywith_for_update(). Для очередей задачSKIP LOCKED, чтобы воркеры не ждали друг друга; - оптимистическая блокировка: колонка
version, обновлениеWHERE version = :old, при нуле изменённых строк повтор. В SQLAlchemy есть встроенныйversion_id_col; - ограничения: уникальный индекс и
INSERT ... ON CONFLICTне дадут создать дубль при одновременных запросах, ключ идемпотентности защищает от повторной отправки; - advisory locks в PostgreSQL:
pg_advisory_xact_lock(key)блокирует произвольный ключ, а не строку.
Вне базы: распределённая блокировка в Redis (SET key token NX PX 30000) или сериализация через очередь, когда все операции по одному ключу обрабатывает один потребитель, например одна партиция Kafka.
Где спотыкаются
Предлагают threading.Lock для веб-приложения. Он не видит другие процессы, и гонка между воркерами остаётся.
Вызывают select_for_update() вне transaction.atomic(). Django выбросит TransactionManagementError, а в режиме autocommit блокировка снимается сразу после запроса.
Ставят блокировку в Redis без TTL или с TTL короче операции. В первом случае упавший процесс держит её вечно, во втором два процесса работают одновременно. Снимать блокировку можно только со своим токеном, а для строгих гарантий нужен fencing token.