Как обеспечить доступ при двух транзакциях к одной строке в базе данных
Что проверяют
Понимаете ли вы, что конкурентный доступ решает база, а не Python, и какие инструменты она даёт: блокировки строк, уровни изоляции, атомарные обновления. Типичный сценарий: два запроса одновременно списывают деньги с одного баланса. Следом спрашивают, как вы переживёте deadlock и что делать с ошибкой сериализации.
Как отвечать
Сначала назовите проблему: два процесса читают баланс 100, каждый вычитает 30 и пишет 70, одно списание потеряно. Это lost update. Дальше способы, от простого к сложному:
- атомарный UPDATE:
UPDATE accounts SET balance = balance - 30 WHERE id = 1 AND balance >= 30. Чтения в приложении нет, гонки тоже. В Django этоF("balance") - 30внутриupdate(); - пессимистичная блокировка:
SELECT ... FOR UPDATEдержит строку до конца транзакции, второй ждёт. В Djangoselect_for_update()строго внутриtransaction.atomic(), в SQLAlchemywith_for_update(). Для очередей задач полезныnowaitиskip_locked; - оптимистичная блокировка: колонка
version, запись черезUPDATE ... WHERE id = ? AND version = ?и проверкаrowcount. Ноль строк значит, что вас опередили, и попытка повторяется. В SQLAlchemy естьversion_id_col, при конфликте летитStaleDataError; - уровни изоляции: в PostgreSQL по умолчанию Read Committed. На Repeatable Read и Serializable база не предотвращает конфликт заранее, а откатывает одну из транзакций с ошибкой сериализации (SQLSTATE
40001), и повтор ложится на приложение.
Про deadlock: PostgreSQL сам находит взаимную блокировку и прерывает одну из транзакций. Профилактика это одинаковый порядок захвата строк, например по возрастанию id, и короткие транзакции без сетевых вызовов внутри.
Где спотыкаются
Пишут FOR UPDATE для SQLite. Там такого синтаксиса нет, SQLite блокирует запись во всю базу целиком, и пример просто не выполнится.
Вызывают select_for_update() вне atomic(). В режиме autocommit блокировка снялась бы сразу, поэтому Django бросает TransactionManagementError.
Думают, что Serializable «убирает конфликты». Он их обнаруживает и откатывает транзакцию, и без цикла повторов пользователь получает ошибку.