> Как решить проблему race condition на примере системы голосования? (Python)

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

Компании: HeadHunter

Стек: Python

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

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

Race condition в системе голосования возникает, когда несколько запросов одновременно читают и записывают один счётчик голосов, что приводит к потерянным обновлениям. Решение - атомарные операции на уровне БД (например, UPDATE ... SET votes = votes + 1), транзакции с правильным уровнем изоляции, или оптимистичная блокировка с версионированием. Для высоконагруженных систем - очередь сообщений или шардирование счётчика. Главное - не использовать read-modify-write на уровне приложения без синхронизации.

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

Проблема классическая: два пользователя голосуют одновременно. Оба читают значение votes = 10, оба инкрементируют в памяти до 11, оба записывают 11. Результат - потерян один голос.

Базовые подходы:

  1. Атомарный UPDATE - самый простой и надёжный способ для одного счётчика:

    SQL
    UPDATE votes SET count = count + 1 WHERE poll_id = ?

    БД гарантирует атомарность операции на уровне строки. Никаких гонок.

  2. Транзакции с SELECT FOR UPDATE - если нужно не только инкрементировать, но и проверить другие условия (например, что пользователь ещё не голосовал):

    SQL
    BEGIN;
    SELECT * FROM votes WHERE poll_id = ? FOR UPDATE;
    -- проверка и обновление
    COMMIT;

    Блокировка строки на время транзакции. Минус - риск deadlock при множественных блокировках.

  3. Оптимистичная блокировка - добавляем поле version:

    SQL
    UPDATE votes SET count = count + 1, version = version + 1
    WHERE poll_id = ? AND version = ?

    Если rowcount = 0 - конфликт, повторяем попытку. Хорошо для низкой конкуренции.

  4. Очередь сообщений - для очень высоких нагрузок. Голоса складываются в очередь, consumer'ы обрабатывают их последовательно или партиями. Минус - сложность инфраструктуры и задержка.

  5. Шардирование счётчика - разбиваем один счётчик на N независимых, потом суммируем. Позволяет распараллелить запись, но усложняет чтение.

Важно понимать trade-off: атомарный UPDATE - самый простой, но не решает проблему "пользователь голосует дважды". Для этого нужна уникальная связка (poll_id, user_id) и обработка ошибки дубликата.

На практике

Для типичного backend на Python (FastAPI/Flask + PostgreSQL) рекомендую:

  • Использовать один атомарный UPDATE для инкремента счётчика.
  • Для проверки "не голосовал ли уже" - вставлять запись в таблицу user_votes с уникальным индексом (poll_id, user_id) и ловить IntegrityError.
  • Оборачивать обе операции в транзакцию, но не держать её открытой долго.
  • Если нужна высокая точность и нагрузка большая - рассмотреть Redis INCR для счётчика и отдельную проверку дубликатов в Postgres.

Пример на Python с SQLAlchemy:

PYTHON
from sqlalchemy.exc import IntegrityError
async def vote(session, poll_id, user_id):
# 1. Проверка дубликата через уникальный индекс
try:
session.add(UserVote(poll_id=poll_id, user_id=user_id))
await session.flush() # тут будет IntegrityError при дубликате
except IntegrityError:
await session.rollback()
return False # уже голосовал
# 2. Атомарный инкремент
await session.execute(
update(Poll)
.where(Poll.id == poll_id)
.values(votes=Poll.votes + 1)
)
await session.commit()
return True

Как отвечать на собеседовании

Начни с формулировки проблемы: "Race condition - это когда два потока читают одно значение, модифицируют и пишут обратно, теряя одно обновление". Затем перечисли решения от простого к сложному. Обязательно упомяни, что атомарный UPDATE - базовое решение, но для проверки дубликатов нужен уникальный индекс. Покажи понимание trade-off: блокировки vs оптимистичность vs очереди. Если спросят про Redis - объясни, что INCR атомарен, но не решает проблему дубликатов. Заверши практическим примером на Python.

Что проверяет интервьюер

  • Понимание гонки данных и почему read-modify-write небезопасен.
  • Знание механизмов БД: атомарные операции, блокировки, уровни изоляции.
  • Умение выбирать решение под нагрузку и требования.
  • Практический опыт: обработка ошибок, транзакции, уникальные индексы.
  • Понимание ограничений каждого подхода.

Типичные ошибки

  • Предложение использовать threading.Lock или asyncio.Lock - это работает только в рамках одного процесса, не решает проблему при нескольких воркерах.
  • Игнорирование проверки дубликатов - считают, что атомарный UPDATE решает всё.
  • Использование SELECT + UPDATE без блокировки и без версии - классическая гонка.
  • Держать транзакцию открытой долго - увеличивает шанс deadlock и снижает пропускную способность.
  • Не обрабатывать IntegrityError при вставке дубликата - падение всего запроса вместо graceful handling.

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

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