> Как решить проблему race condition на примере системы голосования? (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: HeadHunter
Стек: Python
> Пример ответа
Короткий ответ
Race condition в системе голосования возникает, когда несколько запросов одновременно читают и записывают один счётчик голосов, что приводит к потерянным обновлениям. Решение - атомарные операции на уровне БД (например, UPDATE ... SET votes = votes + 1), транзакции с правильным уровнем изоляции, или оптимистичная блокировка с версионированием. Для высоконагруженных систем - очередь сообщений или шардирование счётчика. Главное - не использовать read-modify-write на уровне приложения без синхронизации.
Подробное объяснение
Проблема классическая: два пользователя голосуют одновременно. Оба читают значение votes = 10, оба инкрементируют в памяти до 11, оба записывают 11. Результат - потерян один голос.
Базовые подходы:
-
Атомарный UPDATE - самый простой и надёжный способ для одного счётчика:
SQLUPDATE votes SET count = count + 1 WHERE poll_id = ?БД гарантирует атомарность операции на уровне строки. Никаких гонок.
-
Транзакции с SELECT FOR UPDATE - если нужно не только инкрементировать, но и проверить другие условия (например, что пользователь ещё не голосовал):
SQLBEGIN;SELECT * FROM votes WHERE poll_id = ? FOR UPDATE;-- проверка и обновлениеCOMMIT;Блокировка строки на время транзакции. Минус - риск deadlock при множественных блокировках.
-
Оптимистичная блокировка - добавляем поле
version:SQLUPDATE votes SET count = count + 1, version = version + 1WHERE poll_id = ? AND version = ?Если
rowcount = 0- конфликт, повторяем попытку. Хорошо для низкой конкуренции. -
Очередь сообщений - для очень высоких нагрузок. Голоса складываются в очередь, consumer'ы обрабатывают их последовательно или партиями. Минус - сложность инфраструктуры и задержка.
-
Шардирование счётчика - разбиваем один счётчик на 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:
PYTHONfrom sqlalchemy.exc import IntegrityErrorasync 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.
> Похожие задачи по Python
Помогает ли транзакция с уровнем изоляции сериализации при race condition?
Как решить проблему гонки при переводе денег между счетами?
С какими Python фреймворками кроме Django вы работали?
Какой методологии разработки вы придерживаетесь: Канбан или Скрам?
> Похожие задачи по backend
Помогает ли транзакция с уровнем изоляции сериализации при race condition?
Как решить проблему гонки при переводе денег между счетами?
С какими Python фреймворками кроме Django вы работали?
Какой методологии разработки вы придерживаетесь: Канбан или Скрам?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью