Как синхронизировать обращения к базе данных при большом количестве одновременных запросов
Что проверяют
Различаете ли вы две задачи: не испортить данные при гонке и не положить базу количеством соединений. Следом обычно спрашивают, как списать деньги с баланса, если два запроса пришли одновременно, и сколько соединений держать в пуле.
Как отвечать
Корректность обеспечивает сама база, а не Python. threading.Lock защищает только потоки одного процесса, а у gunicorn или uvicorn несколько воркеров и обычно несколько подов. Инструменты по возрастанию цены:
- атомарный запрос:
UPDATE accounts SET balance = balance - %s WHERE id = %s AND balance >= %sи проверка числа изменённых строк. Никакого чтения и записи в два шага; - ограничения: уникальный индекс и
INSERT ... ON CONFLICTвместо проверки «есть ли уже такая запись»; - пессимистичная блокировка:
SELECT ... FOR UPDATE, в SQLAlchemy.with_for_update(), в Djangoselect_for_update()внутриtransaction.atomic(). Для очередей задачFOR UPDATE SKIP LOCKED; - оптимистичная блокировка: колонка версии, в SQLAlchemy
version_id_col, при конфликте повтор; - уровень изоляции
SERIALIZABLE: PostgreSQL сам находит конфликты и откатывает транзакцию с SQLSTATE40001, приложение обязано её повторить.
Пропускная способность решается отдельно. В PostgreSQL каждое соединение это процесс, max_connections по умолчанию 100. Пул SQLAlchemy по умолчанию держит pool_size=5 и max_overflow=10 на процесс, умножьте на число воркеров и подов. При большом числе инстансов ставят PgBouncer в режиме transaction pooling. В асинхронном коде на asyncpg конкурентность ограничивают asyncio.Semaphore или размером пула, а тяжёлую запись уводят в очередь, например Celery.
Где спотыкаются
Защищают данные threading.Lock или надеются на GIL. На втором воркере гонка возвращается.
Держат транзакцию открытой во время внешнего HTTP-вызова. Блокировки висят секундами, соединения пула кончаются, и запросы встают в очередь.
Раздувают пул до сотен соединений «для скорости». База тратит память и время на переключение процессов и становится медленнее, а не быстрее.