Как избежать гонки значений в базе данных
Что проверяют
Понимаете ли вы, что транзакция сама по себе гонку не лечит: два воркера на уровне Read Committed спокойно прочитают одно значение и оба запишут своё. Следом обычно просят показать решение в Django ORM или SQLAlchemy и спрашивают про дедлоки.
Как отвечать
Начните с классического примера: прочитать остаток, вычесть в Python, записать обратно. Между чтением и записью другой процесс успевает сделать то же самое, и одно списание теряется (lost update). Дальше способы, от простого к тяжёлому:
- атомарный UPDATE: считать на стороне базы, а не в Python. В Django это
F():filter(pk=pk, stock__gte=qty).update(stock=F("stock") - qty), число обновлённых строк скажет, хватило ли остатка; - пессимистичная блокировка:
SELECT ... FOR UPDATEвнутри транзакции. В Djangoselect_for_update()внутриtransaction.atomic(), в SQLAlchemywith_for_update(). Вторая транзакция ждёт коммита первой; - оптимистичная блокировка: колонка
versionи обновление с условиемWHERE id = ... AND version = .... Затронуто 0 строк, значит кто-то успел раньше, и операцию повторяют. В SQLAlchemy для этого естьversion_id_col; - ограничения в схеме:
UNIQUE,CHECK (stock >= 0)иINSERT ... ON CONFLICTотсекают гонку на уровне базы, даже если код ошибся; - уровень изоляции:
SERIALIZABLEв PostgreSQL ловит аномалии сам, но отвечает ошибкой сериализации, и код обязан повторить транзакцию; - advisory locks:
pg_advisory_xact_lock, когда блокировать нужно не строку, а операцию целиком.
Закончите выбором: при редких конфликтах оптимистичная схема дешевле, при частых на одной строке честнее FOR UPDATE.
Где спотыкаются
Считают, что transaction.atomic() уже защищает. Атомарность означает «всё или ничего», а не «никто не читал одновременно со мной»; на Read Committed, уровне по умолчанию в PostgreSQL, lost update остаётся.
Предлагают threading.Lock. Он работает внутри одного процесса, а у вас несколько воркеров Gunicorn или Celery на разных машинах.
Забывают про дедлоки и повторы. Строки нужно блокировать в одном порядке, например по возрастанию id, а ошибки сериализации и дедлока ловить и повторять транзакцию целиком.