> Как решить проблему гонки при переводе денег между счетами? (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: HeadHunter
Стек: Python
> Пример ответа
Короткий ответ
Гонка при переводе денег решается через атомарность операции: либо блокировка строк в БД (SELECT ... FOR UPDATE), либо оптимистичная блокировка с версионированием, либо атомарные UPDATE-запросы с проверкой баланса. В распределённых системах - saga или outbox. Ключевое - исключить чтение-модификацию-запись без изоляции. В Python с PostgreSQL чаще всего используют SELECT FOR UPDATE в транзакции или UPDATE ... SET balance = balance - X WHERE id = ... AND balance >= X.
Подробное объяснение
Гонка возникает, когда два параллельных запроса читают баланс, вычитают сумму и записывают результат - итог теряет одно обновление. Решения делятся на два класса:
-
Пессимистичная блокировка - захват строки на время транзакции. В PostgreSQL:
SELECT ... FOR UPDATE. Другие транзакции ждут или получают ошибку. Гарантирует сериализацию, но снижает пропускную способность и требует коротких транзакций. -
Оптимистичная блокировка - версия или условие в
UPDATE. Например,UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100. Если строка изменилась -rowcount = 0, повторяем или возвращаем ошибку. Не блокирует чтение, но требует обработки конфликтов. -
Атомарные операции на уровне СУБД - инкремент/декремент в одном выражении. Это частный случай оптимистичной блокировки, но без отдельной проверки версии.
Для переводов между двумя счетами важна атомарность всей операции: списание и зачисление должны быть в одной транзакции. Иначе возможен частичный перевод. В высоконагруженных системах используют компенсационные транзакции (saga) или шардирование с локальными блокировками.
На практике
Для типового backend на Python (FastAPI/Flask + SQLAlchemy/asyncpg) с PostgreSQL:
- Используй
SELECT ... FOR UPDATEдля обоих счетов в строго определённом порядке (например, по id), чтобы избежать deadlock. - Проверяй баланс после блокировки, а не до.
- Держи транзакцию короткой: без внешних вызовов, без длительных вычислений.
- Для высокого конкурентного чтения - оптимистичная блокировка через
versionили условие вUPDATE. - В Redis-кэше не храни баланс как источник истины - только как кэш.
Пример кода
PYTHONfrom sqlalchemy import select, updatefrom sqlalchemy.orm import Sessiondef transfer_money(session: Session, from_id: int, to_id: int, amount: int):# Фиксируем порядок блокировки, чтобы избежать deadlockfirst_id, second_id = sorted([from_id, to_id])# Блокируем обе строкиaccounts = session.execute(select(Account).where(Account.id.in_([first_id, second_id])).with_for_update()).scalars().all()account_map = {acc.id: acc for acc in accounts}from_acc = account_map[from_id]to_acc = account_map[to_id]if from_acc.balance < amount:raise InsufficientFunds()from_acc.balance -= amountto_acc.balance += amountsession.commit()
Оптимистичный вариант:
PYTHONdef transfer_optimistic(session: Session, from_id: int, to_id: int, amount: int):# Сначала списание с проверкой балансаresult = session.execute(update(Account).where(Account.id == from_id, Account.balance >= amount).values(balance=Account.balance - amount))if result.rowcount == 0:raise InsufficientFunds()# Затем зачислениеsession.execute(update(Account).where(Account.id == to_id).values(balance=Account.balance + amount))session.commit()
Как отвечать на собеседовании
Начни с формулировки проблемы: гонка - это потерянное обновление. Затем предложи два подхода - пессимистичный и оптимистичный. Объясни trade-off: блокировки снижают параллелизм, но проще; оптимизм - быстрее при редких конфликтах, но требует retry. Упомяни deadlock и порядок блокировок. Если спросят про распределённые системы - расскажи про saga и идемпотентность. Покажи код на Python, но не увлекайся деталями ORM - интервьюеру важна логика.
Что проверяет интервьюер
- Понимание атомарности и изоляции транзакций.
- Умение видеть гонку в чтении-модификации-записи.
- Знание механизмов СУБД:
FOR UPDATE,rowcount, версионирование. - Осознание deadlock и способов его избежать.
- Умение объяснить trade-off между производительностью и консистентностью.
Типичные ошибки
- Проверка баланса до блокировки - гонка остаётся.
- Блокировка только одного счёта - второй может измениться.
- Разные порядки блокировок в разных вызовах - deadlock.
- Долгие операции внутри транзакции - рост числа ожидающих.
- Игнорирование
rowcountпри оптимистичной блокировке - потеря обновления. - Перевод без транзакции - частичное списание при ошибке.
> Похожие задачи по Python
Как решить проблему повторного запроса при переводе денег, чтобы избежать двойного списания?
Помогает ли транзакция с уровнем изоляции сериализации при race condition?
Как решить проблему race condition на примере системы голосования?
С какими Python фреймворками кроме Django вы работали?
> Похожие задачи по backend
Как решить проблему повторного запроса при переводе денег, чтобы избежать двойного списания?
Помогает ли транзакция с уровнем изоляции сериализации при race condition?
Как решить проблему race condition на примере системы голосования?
С какими Python фреймворками кроме Django вы работали?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью