> Как решить проблему гонки при переводе денег между счетами? (Python)

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

Компании: HeadHunter

Стек: Python

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

Для решения проблемы гонки (race condition) при переводе денег между счетами в Python необходимо обеспечить атомарность операции. Основные подходы:

  1. Использование блокировок на уровне БД (рекомендуемый способ):
PYTHON
from django.db import transaction
@transaction.atomic
def transfer_money(from_account_id, to_account_id, amount):
# SELECT ... FOR UPDATE блокирует строки до конца транзакции
from_account = Account.objects.select_for_update().get(pk=from_account_id)
to_account = Account.objects.select_for_update().get(pk=to_account_id)
if from_account.balance < amount:
raise ValueError("Недостаточно средств")
from_account.balance -= amount
to_account.balance += amount
from_account.save()
to_account.save()
  1. Оптимистическая блокировка (через поле version):
PYTHON
from django.db import transaction
def transfer_money_optimistic(from_account_id, to_account_id, amount):
max_retries = 3
for attempt in range(max_retries):
try:
with transaction.atomic():
from_account = Account.objects.get(pk=from_account_id)
to_account = Account.objects.get(pk=to_account_id)
if from_account.balance < amount:
raise ValueError("Недостаточно средств")
updated = Account.objects.filter(
pk=from_account_id,
version=from_account.version
).update(
balance=from_account.balance - amount,
version=from_account.version + 1
)
if updated == 0:
raise Account.DoesNotExist # вызовет повтор
Account.objects.filter(
pk=to_account_id,
version=to_account.version
).update(
balance=to_account.balance + amount,
version=to_account.version + 1
)
break
except Account.DoesNotExist:
if attempt == max_retries - 1:
raise
continue
  1. Redis-блокировка для распределенных систем:
PYTHON
import redis
import time
r = redis.Redis()
def transfer_with_lock(from_id, to_id, amount):
lock_key = f"transfer_lock:{min(from_id, to_id)}:{max(from_id, to_id)}"
lock = r.lock(lock_key, timeout=10)
if lock.acquire(blocking=True, blocking_timeout=5):
try:
# выполнить перевод
pass
finally:
lock.release()

Ключевые моменты:

  • Всегда используйте SELECT ... FOR UPDATE или эквивалент для пессимистичной блокировки
  • Проверяйте баланс внутри той же транзакции, где происходит списание
  • Избегайте deadlock'ов - блокируйте счета в фиксированном порядке (например, по ID)
  • Для высоконагруженных систем рассмотрите шардирование счетов

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

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