> Будет ли конкуренция при выполнении периодического таска, который читает и обновляет записи в таблице? (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: HeadHunter
Стек: Python
> Пример ответа
Короткий ответ
Да, конкуренция возможна. Если периодический таск выполняется в нескольких воркерах или перекрывается по времени с предыдущим запуском, два процесса могут одновременно читать и обновлять одни и те же записи. Это приводит к гонкам (race conditions), потерянным обновлениям и нарушению целостности данных. Проблема усугубляется, если таск использует read-modify-write циклы без транзакций или блокировок. Решение зависит от СУБД и требований к консистентности.
Подробное объяснение
Конкуренция возникает на нескольких уровнях:
-
Перекрытие запусков - если интервал таска меньше времени его выполнения, предыдущий запуск ещё не завершился, а следующий уже стартовал. Это классическая проблема в Celery beat, APScheduler и подобных системах.
-
Несколько воркеров - даже при неперекрывающихся запусках, если таск разворачивается на нескольких нодах, они могут выполнять один и тот же периодический джоб параллельно.
-
Внутренняя конкурентность - внутри одного таска может быть конкурентный доступ к одним записям, если таск обрабатывает набор строк и обновляет их в цикле, а другой процесс (не обязательно периодический) работает с теми же строками.
Основные проблемы:
- Lost update - два процесса читают значение, оба модифицируют, последний записывает, перезаписывая изменения первого.
- Dirty read - процесс читает данные, которые другой процесс ещё не закоммитил.
- Non-repeatable read - в рамках одной транзакции одно и то же чтение даёт разные результаты.
Для периодических тасков особенно опасен сценарий: таск читает записи со статусом pending, обрабатывает их и меняет статус на processed. Если два экземпляра таска запустятся одновременно, оба прочитают один и тот же набор pending записей и обработают их дважды.
На практике
Стандартные подходы к решению:
- Блокировка на уровне приложения - использовать distributed lock (Redis, ZooKeeper, PostgreSQL advisory locks), чтобы гарантировать, что только один инстанс таска выполняет работу в данный момент.
- Оптимистичная блокировка - добавить поле
versionилиupdated_atв таблицу и проверять его при обновлении. Если версия изменилась - значит, запись уже обработана другим процессом. - Атомарные UPDATE с условием - например,
UPDATE ... WHERE status = 'pending' RETURNING id- это атомарная операция, которая сразу резервирует записи за текущим процессом. - SELECT ... FOR UPDATE SKIP LOCKED - в PostgreSQL позволяет выбрать только те строки, которые не заблокированы другими транзакциями, и заблокировать их для себя.
- Идемпотентность - сделать обработку идемпотентной, чтобы повторное выполнение не приводило к негативным последствиям.
Для периодических тасков в Celery обычно используют task_soft_time_limit и проверку на перекрытие через Redis-лок с TTL, равным максимальному времени выполнения таска.
Пример кода
PYTHON# Пример с PostgreSQL advisory lock в Celery taskimport psycopg2from celery import shared_task@shared_task(bind=True, max_retries=3)def process_pending_records(self):conn = psycopg2.connect(dsn)try:with conn:with conn.cursor() as cur:# advisory lock - гарантирует, что только один процесс# выполняет таск на уровне всей БДcur.execute("SELECT pg_advisory_lock(42)")try:cur.execute("""UPDATE recordsSET status = 'processing'WHERE id IN (SELECT id FROM recordsWHERE status = 'pending'LIMIT 100)RETURNING id""")ids = [row[0] for row in cur.fetchall()]# обработка ids...finally:cur.execute("SELECT pg_advisory_unlock(42)")except Exception as exc:raise self.retry(exc=exc)
Альтернатива с SKIP LOCKED:
PYTHON@shared_taskdef process_batch():with db_session() as session:rows = session.execute("""SELECT * FROM recordsWHERE status = 'pending'ORDER BY idLIMIT 100FOR UPDATE SKIP LOCKED""").fetchall()# обработка rows...
Как отвечать на собеседовании
Начни с прямого ответа - да, конкуренция возможна и это серьёзная проблема. Затем перечисли сценарии, при которых она возникает: перекрытие запусков, несколько воркеров, параллельные процессы. Объясни, какие проблемы это вызывает - гонки, потерянные обновления, дублирование обработки.
Затем переходи к решениям. Сначала упомяни самый простой и надёжный вариант - advisory lock или distributed lock. Потом - атомарные операции с UPDATE ... RETURNING или SKIP LOCKED. И обязательно упомяни идемпотентность как защитный механизм.
Покажи понимание trade-off: блокировки снижают пропускную способность, но гарантируют консистентность; идемпотентность не решает проблему гонок, но делает последствия менее критичными.
Если спросят про конкретную СУБД - расскажи про SELECT ... FOR UPDATE SKIP LOCKED в PostgreSQL, UPDLOCK в SQL Server, SELECT ... FOR UPDATE в MySQL.
Что проверяет интервьюер
Интервьюер проверяет:
- Понимание базовых концепций конкурентности: race condition, lost update, транзакции, уровни изоляции.
- Умение видеть проблемы в реальных сценариях, а не только в теории.
- Знание инструментов решения: блокировки, атомарные операции, идемпотентность.
- Понимание ограничений каждого подхода - например, что distributed lock - это внешняя зависимость, а
SKIP LOCKEDработает не во всех СУБД. - Способность рассуждать о trade-off между производительностью и консистентностью.
Типичные ошибки
- Ответ "нет, конкуренции не будет" - это сразу провал, потому что конкуренция есть всегда, если не приняты меры.
- Утверждение, что транзакции решают все проблемы - транзакции не защищают от гонок между независимыми транзакциями без правильного уровня изоляции и блокировок.
- Предложение использовать только
SELECT ... FOR UPDATEбезSKIP LOCKED- это заблокирует все строки и сериализует обработку, что может быть неприемлемо по производительности. - Игнорирование перекрытия запусков - если таск выполняется дольше, чем интервал, проблема возникает даже без нескольких воркеров.
- Отсутствие упоминания идемпотентности - это важный защитный слой, который часто упускают.
> Похожие задачи по Python
Чем занимались на прошлом месте работы?
Работали ли с CDC системами, например Debezium, для мониторинга изменений в базе данных и интеграции с асинхронными процессами?
Как работает периодический таск, который читает таблицу уведомлений и отправляет их?
Какие решения кроме паттерна outbox помогают гарантировать выполнение асинхронных задач?
> Похожие задачи по backend
Чем занимались на прошлом месте работы?
Работали ли с CDC системами, например Debezium, для мониторинга изменений в базе данных и интеграции с асинхронными процессами?
Как работает периодический таск, который читает таблицу уведомлений и отправляет их?
Какие решения кроме паттерна outbox помогают гарантировать выполнение асинхронных задач?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью