Этот вопрос проверяет понимание методов синхронизации при работе с критичными ресурсами, такими как балансы, лимиты, инвентарь.
Для корректной работы с балансами применяют транзакции с блокировками строк (SELECT FOR UPDATE), оптимистическую блокировку с версионированием, распределённые блокировки (например, Redis Redlock), очереди событий, а также архитектурный принцип «один владелец ресурса». Выбор подхода зависит от нагрузки: для локальной БД подходит транзакционное обновление, а для распределённых систем используют очереди, шардирование и отдельный сервис-владелец балансов. Главная цель — сделать операции атомарными, последовательными и идемпотентными, чтобы избежать гонок и потерь данных.
Определение.
Критически важный конкурентный ресурс — это данные, которые нельзя обновлять одновременно несколькими процессами без риска ошибки (например, баланс счёта, количество товара, лимиты).
Основные угрозы:
Гонки данных (race conditions).
Потеря обновлений.
Двойное списание.
Неконсистентные состояния.
Самый распространённый способ:
Начинаем транзакцию.
Делаем выборку строки с блокировкой:
SELECT * FROM accounts WHERE id = 123 FOR UPDATE;
Проверяем баланс.
Делаем UPDATE.
Коммитим транзакцию.
Преимущества:
Простой, надёжный механизм.
Работает при средней нагрузке.
Недостатки:
Блокировки могут замедлять систему.
Возможны deadlock при неправильном порядке.
Идея:
Каждая запись имеет поле version или updated_at.
Читаем запись → получаем version = X.
Пытаемся обновить:
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 123 AND version = X;
Если обновлено 0 строк — конфликт → повторяем операцию.
Плюсы:
Нет тяжёлых блокировок.
Высокая производительность.
Минусы:
Нужно уметь повторять попытки безопасно.
Если ресурс разделён между несколькими сервисами:
Используют Redis Redlock, ZooKeeper locks, Consul locks.
Важны гарантии:
TTL блокировки.
Обновление lease.
Предотвращение split-brain.
Пример Redis:
Python
lock = redis.lock("balance:123", timeout=5)
with lock:
update_balance()
Архитектурный подход:
Только один сервис отвечает за обновление ресурса.
Все остальные посылают ему запросы/команды.
Состояние изменяется строго последовательно.
Преимущества:
Невозможны гонки между сервисами.
Упрощает логику.
Недостатки:
Увеличивает нагрузку на сервис-владелец.
Нужна высокая доступность.
Все изменения баланса превращаются в события (например, "debit 100").
События помещаются в очередь (Kafka/Rabbit).
Консьюмер обрабатывает их строго последовательно.
Плюсы:
Очень надёжно и масштабируемо.
Естественная история изменений.
Минусы:
Сложнее реализовать.
Требует сильной дисциплины.
Если много конкурирующих операций:
Разделить ресурсы по шардам.
Каждый шар обрабатывается отдельным воркером.
Баланс/инвентарь обновляются только «назначенным» воркером.
Это уменьшает конкуренцию и распределяет нагрузку.
Каждая операция должна иметь operation_id.
Выполнение операции проверяется по этому ID.
Повторы не меняют финальное состояние.
Часто важно:
Лимитировать количество обработок.
Логировать все попытки.
Пример:
UPDATE accounts
SET balance = balance - 100
WHERE id = 123 AND balance >= 100;
Если обновлено 1 строка — успех.
Если 0 — недостаточно средств.
Для корректной работы с критичными ресурсами нужно гарантировать последовательность и атомарность операций. В локальной базе это достигается транзакциями и блокировками. В распределённой системе используются распределённые мьютексы, сервис-владелец балансов или очереди событий. Часто сочетают несколько подходов: идемпотентные операции, атомарные обновления и контроль версий записи.