youmixx
@youmixx
PHP Developer

Laravel как правильно обновить баланс при покупке?

Всем привет.
Кратко опишу проблему, т.к. хочу сам разобраться и понять. Если что - скину фрагменты кода.

Есть сайт. На нем есть покупка товара (чего - не важно).
Есть контролер, в нем метод buyProduct(). Там все по дефолту, сначала идут разные проверки (хватает ли баланса, есть ли еще товар, не заблочен ли юзер и т.п.).

Если все проверки проходят, идет try catch блок с beginTransaction, commit-ом в случае успеха ну и rollback в случае ошибки.

Всего идут 3 sql запроса (через конструктор запросов Laravel).
1 - Занесение инфы в таблицу buyHistory.
2 - Занесение инфы в таблицу historyPayment (история оплат, не так важно зачем мне она, просто говорю все как есть).
3 - Уменьшение баланса юзера на цену товара.

Проблема с третим пунктом. Раньше я обновлял баланс так:
$user->balance -= $price;
$user->saveOrFail().


Но была проблема с тем, что когда очень быстро отсылаешь запросы на покупку (у сайта есть API, через него можно покупать). То бывал часто баг, когда баланс еще не успевал минуснуться, уже шел новый запрос. Сайт думал что баланса хватало и выдавал еще один товар (хотя по факту денег уже не хватало, не успевал пройти запрос).

Я пофиксил это легко - сделал так:
$user->dispatch('balance', $price);

И все стало просто круто, все работало. Но сейчас проблема появилась опять.
(довольно много юзеров на сайте, а не давно еще вырос онлайн). Просто если раньше словить баг было очень легко и наабузить себе баланс. То сейчас чуть сложнее, нужно еще быстрее обновлять страницу и где-то 1 попытка из 3 сработает.

Я вывел лог, проблема таже - не успевает минуснуться баланс.
Мне нужна подсказка: как правильно построить эту систему покупки, чтобы все было хорошо?
  • Вопрос задан
  • 535 просмотров
Решения вопроса 3
iMedved2009
@iMedved2009
Не люблю людей
1. Лочить запись в таблице балансов до момента списания. Залочили, проверили, списали, разлочили. Другие процессы либо будут ждать - либо вылетят по таймауту.

2. Использовать update с условием. update user_balance where user_id = ? and balance > нужного. У вас запрос не выполнится если кто то уже списал деньги. А вы по affected rows можете судить списалось или нет
Ответ написан
alexey-m-ukolov
@alexey-m-ukolov Куратор тега PHP
Нужно просто для всех команд списания с баланса использовать мьютекс, внутри которого:
1. Получать текущий баланс.
2. Проверять, что он больше, либо равен сумме списания.
3. Вносить изменения в БД, если всё ок.
Третий шаг можно реализовать по-разному - через таблицу транзакций, через единственное поле баланса, как у вас сейчас или ещё как угодно. Эта часть совершенно не важна.
Важно только то, что функционал списания с баланса в целом становится однопоточным за счёт блокировки.
Нужно только учитывать, что блокировка должна быть распределённой. Стандартом является использование алгоритма Redlock, реализованном на базе Redis.
Ещё важно использовать один блокировщик именно для всех типов списаний. Если вы в buyProduct будете использовать один мьютекс, а в каком-нибудь buyService другой, то работать это правильно не будет.
Ответ написан
Комментировать
rozhnev
@rozhnev Куратор тега PHP
Fullstack programmer, DBA, медленно, дорого
В таблице баланса ограничиваете возможность отрицательных значений
create table user_balance (
	user_id int,
  	balance decimal(9, 2) check (balance >=0)
);


create table user_balance_unsigned (
	user_id int,
  	balance decimal(9, 2) unsigned
);


любая попытка списать больше чем баланс вызывает ошибку

SQL online environment
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы