Как ответить
Баланс партнёра — это критически важные деньги, поэтому основное правило: никаких чисел с плавающей точкой. Используйте DECIMAL(18,2) или NUMERIC — они гарантируют точность до копейки. Второе правило: все операции с балансом должны быть атомарными и выполняться в транзакции. Никогда не читайте баланс в коде приложения, не меняйте его и не пишите обратно — это классический race condition.
Вместо этого делайте атомарный UPDATE с проверкой условия. Например, для списания:
BEGIN;
UPDATE partner_balances
SET balance = balance - 100.50
WHERE partner_id = 42 AND balance >= 100.50;
IF ROW_COUNT() = 0 THEN
ROLLBACK;
-- ошибка: недостаточно средств
ELSE
INSERT INTO balance_operations (partner_id, amount, type, created_at)
VALUES (42, -100.50, 'withdrawal', NOW());
COMMIT;
END IF;Такой подход исключает гонку: UPDATE блокирует строку (InnoDB row lock), а проверка balance >= 100.50 гарантирует, что мы не уйдём в минус. Журнал операций (balance_operations) обязателен — это аудит, возможность отката и восстановления. Каждая операция — отдельная запись с типом, суммой, временем и внешним ключом на партнёра.
Если нужна строгая консистентность (а для денег она нужна), используйте SELECT ... FOR UPDATE только когда логика сложнее простого UPDATE (например, нужно проверить несколько условий или агрегировать данные). Но в большинстве случаев атомарного UPDATE достаточно.
Для высоконагруженных систем можно применить шардинг по partner_id или партиционирование таблицы операций по дате — это ускорит запросы и упростит архивацию. Баланс самого партнёра лучше хранить в отдельной таблице с единственной строкой на партнёра и обновлять её только через UPDATE — никаких INSERT/UPDATE конфликтов.
И последнее: не используйте ORM для обновления баланса. ORM часто читает объект, меняет поле и пишет — это две операции вместо одной. Пишите прямые SQL-запросы или используйте хранимые процедуры.