Списание с баланса без гонок
Вопрос
Как реализовать списание с баланса пользователя так, чтобы при параллельных запросах баланс не ушёл в минус и списания не потерялись?
Попробуйте сами
Ответьте вслух за 2 минуты, как на собесе. Потом сверьтесь с тезисами: что прозвучало, а что нет.
Показать ответ и разбор
Коротко
Что должно прозвучать в ответе
Проблема: read-modify-write в приложении даёт lost update и уход в минус
Атомарный условный UPDATE: SET balance = balance - $1 WHERE id = $2 AND balance >= $1, проверить число затронутых строк
Или SELECT ... FOR UPDATE в транзакции, затем проверка и запись
Или оптимистичная блокировка через колонку version и повтор
CHECK (balance >= 0) как последняя линия защиты
Идемпотентность: ключ операции, чтобы повтор запроса не списал дважды
Что могут спросить следом
- Что выберешь при высокой конкуренции за один счёт?
- Как хранить историю операций, чтобы баланс можно было пересчитать?
Разбор
Главная ошибка: прочитать баланс в приложении, проверить и записать новое значение. Два параллельных запроса оба увидят 100 и оба спишут, одно списание потеряется или баланс уйдёт в минус. Самое простое и надёжное решение: один атомарный UPDATE с условием, SET balance = balance - $1 WHERE id = $2 AND balance >= $1, и проверка, что затронута одна строка; если ноль, денег не хватило. Если логика сложнее, беру строку под SELECT ... FOR UPDATE в транзакции: конкуренты подождут. При редких конфликтах подходит оптимистичная блокировка с колонкой version и повтором. В базе добавляю CHECK (balance >= 0) как страховку. И отдельно идемпотентность: у операции есть уникальный ключ, повтор того же запроса от клиента не спишет второй раз. В серьёзных системах баланс ещё и выводится из журнала операций, а не только хранится числом.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.
Термины по теме
Ещё по теме «Транзакции и изоляция»
- Advisory-блокировки в PostgreSQL
- Как избежать deadlock при переводах
- Взаимоблокировки: как ловить и предотвращать
- Долгая транзакция и раздувание таблиц
- MVCC и VACUUM в PostgreSQL
- VACUUM или VACUUM FULL на проде
- Зачем нужна заморозка и что такое wraparound
- Что делает PostgreSQL при deadlock