Списание баланса пользователя: гонка при конкурентных вызовах

Код-ревьюMiddle15 минпроверка — Спринт14-t030

Дана таблица users(user_id, balance) и псевдокод функции withdrawBalance (два варианта записи одной и той же логики): в транзакции READ COMMITTED она читает баланс, проверяет, что денег хватает, и списывает сумму. Найдите гонку при конкурентных вызовах и всё, что ещё плохо; предложите рефакторинг.

Дополнительно: что будет, если два экземпляра сервиса одновременно получат запрос на списание с одного счёта — как это скажется на консистентности данных?

Как на собесе: 15 минут, ищи всё — гонки, утечки, ошибки, безопасность, стиль. Ссылайся на строки.

review.gogo
1-- Юзеры и их балансы2CREATE TABLE users3(4    user_id INTEGER PRIMARY KEY,5    balance REAL    NOT NULL6);7 8// Вариант 1 (псевдокод). Снятие денег с баланса пользователя9func withdrawBalance(userId int32, amount float32) float32 {10	exec("BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED")11 12	oldBalance = exec("SELECT balance FROM users WHERE user_id = $1", userId)13	if oldBalance >= amount {14		exec("UPDATE users SET balance = balance - $1 WHERE user_id = $2", amount, userId)15	}16	newBalance = exec("SELECT balance FROM users WHERE user_id = $1", userId)17 18	exec("COMMIT")19 20	return newBalance21}22 23// Вариант 2 (псевдокод) — то же самое через объект tx24func withdrawBalance(userId int32, amount) {25	tx := BeginTransaction("ISOLATION LEVEL READ COMMITTED")26 27	oldBalance := tx.exec("SELECT balance FROM users WHERE user_id = $1", userId)28	if oldBalance >= amount {29		tx.exec("UPDATE users SET balance = balance - $2 WHERE user_id = $1", userId, amount)30	}31	newBalance := tx.exec("SELECT balance FROM users WHERE user_id = $1", userId)32 33	tx.Commit()34 35	return newBalance36}
⌘ Enter — проверить