@entermix

Rак правильно организовать списание баланса?

Возьмем к примеру такой сервис:
Пользователь регистрируется на сайте, где может пополнить счет и создать сообщество, гость этого сообщества может запросить некую услугу, которую оплачивает создатель сообщества.

Допустим реализовали это так:
$this->balance -= $sum;
$this->save();

+ идет запись в другую таблицу (причина, сумма, баланс)

Правильно ли это? Допустим есть 100500 гостей, N пользователей запрашивают услугу одновременно, корректно ли произойдет списание средств? Если нет, то как сделать правильно?
  • Вопрос задан
  • 1740 просмотров
Решения вопроса 3
R0dger
@R0dger
Laravel/Yii/2 AngularJs PHP RESTful API
Если у Вас операция НЕ АТОМАРНАЯ, то нужно обернуть в транзакцию
Частично тут описано - php.net/manual/ru/pdo.transactions.php
Ответ написан
Melkij
@Melkij
PostgreSQL DBA
InnoDB все запросы выполняет только в транзакции. Если открытой транзакции не было, то этот запрос неявно оборачивается в транзакцию.
Вот только сами по себе транзакции не помогут, надо ещё правильно ими пользоваться. Тут я бы хотел дополнительно обратить внимание Андрей:
Попробуйте в двух терминалах написать:
сначала begin; в обоих
потом select из таблички. Значения одинаковые, правда?
потом update этой же таблички, сделайте set fieldname = разные значения в терминалах. Второй терминал запрос принял, но не вернул управление, верно?
потом commit; в первом терминале. update из второго терминала сразу же ответил OK.
Теперь сделайте commit во втором терминале и посмотрите, что произошло с данными. Это то, на что вы рассчитывали? Или всё-таки не совсем?

N пользователей запрашивают услугу одновременно, корректно ли произойдет списание средств?

Нет, некорректно, если только вы не в одной транзакции и не читаете баланс специально с select .. for update.
Потому что для выполнения действия $this->balance -= $sum; у вас уже должен быть известен баланс, но это ещё не операция записи.
В итоге у вас было 1000 рублей.
Пришёл один клиент, прочитал баланс, хочет списать 200 рублей. Обновил циферку в PHP, никто ему не мешает.
Пришёл второй клиент, прочитал баланс, хочет списать 100 рублей. Обновил циферку в PHP, никто ему тоже не мешает.
И на шаге save оба отправили запросы на update: один клиент считает, что на балансе осталось 800 рублей, второй - что 900.
Сколько запишется на баланс? 800 или 900, как повезёт. Правильно ли это? Сколько должно было быть? 700 ведь.
Потому что клиенты не мешали друг другу обновлять циферку в PHP.

Как же заставить клиентов не делать глупости?
В простом случае:
update tablename set balance = balance - :amount where balance >= :amount and user_id=:uid
И на приложении проверять affected_rows. Если строка изменена - у пользователя достаточно денег, платёж прошёл. Если изменённых строк нет - вероятно, у пользователя нет столько денег. СУБД разберётся с очерёдностью исполнения и в результате на балансе будет правильная сумма, сколько бы параллельных запросов ни пришло. И, что не менее важно - приложение ответит на все запросы корректно, кому денег хватило, а кому - уже нет.

В более сложных случаях - можно самому попросить СУБД взять блокировку на строку, о чём чуть ранее я уже заикался:
begin;
select balance from tablename where user_id=:uid for update; -- все параллельные транзакции будут выстраиваться в очередь здесь
/* произвольные запросы (в mysql кроме вызова хранимок, DDL - они делают неявный коммит). Среди этих запросов - обновляете баланс */
commit; -- только здесь эта транзакция освобождает блокировку select .. for update и с этой строкой начинает работать следующий запрос

Как вообще работать с деньгами и делать это правильно - Кирилл даёт правильное направление. Изучите, как это делает бухгалтерия, за многие десятилетия работы они придумали, как обходить много странных граблей.
Ответ написан
@kshvakov
Никогда так не делайте. Считайте остаток баланса на сервере СУБД в запросе на апдейт
PS: как работать со списаниями можно почитать, например, начав отсюда https://ru.wikipedia.org/wiki/%D0%94%D0%B5%D0%B1%D...
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

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

Войти через центр авторизации
Похожие вопросы