> Будет ли конкуренция при выполнении периодического таска, который читает и обновляет записи в таблице? (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: HeadHunter

Стек: Python

> Пример ответа

Короткий ответ

Да, конкуренция возможна. Если периодический таск выполняется в нескольких воркерах или перекрывается по времени с предыдущим запуском, два процесса могут одновременно читать и обновлять одни и те же записи. Это приводит к гонкам (race conditions), потерянным обновлениям и нарушению целостности данных. Проблема усугубляется, если таск использует read-modify-write циклы без транзакций или блокировок. Решение зависит от СУБД и требований к консистентности.

Подробное объяснение

Конкуренция возникает на нескольких уровнях:

  1. Перекрытие запусков - если интервал таска меньше времени его выполнения, предыдущий запуск ещё не завершился, а следующий уже стартовал. Это классическая проблема в Celery beat, APScheduler и подобных системах.

  2. Несколько воркеров - даже при неперекрывающихся запусках, если таск разворачивается на нескольких нодах, они могут выполнять один и тот же периодический джоб параллельно.

  3. Внутренняя конкурентность - внутри одного таска может быть конкурентный доступ к одним записям, если таск обрабатывает набор строк и обновляет их в цикле, а другой процесс (не обязательно периодический) работает с теми же строками.

Основные проблемы:

  • 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 task
import psycopg2
from 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 records
SET status = 'processing'
WHERE id IN (
SELECT id FROM records
WHERE 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_task
def process_batch():
with db_session() as session:
rows = session.execute("""
SELECT * FROM records
WHERE status = 'pending'
ORDER BY id
LIMIT 100
FOR 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 - это заблокирует все строки и сериализует обработку, что может быть неприемлемо по производительности.
  • Игнорирование перекрытия запусков - если таск выполняется дольше, чем интервал, проблема возникает даже без нескольких воркеров.
  • Отсутствие упоминания идемпотентности - это важный защитный слой, который часто упускают.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью