Как избежать race condition при переводе

212
05 мая 2018, 15:29

Например, нужно сделать трансфер от одного пользователя на другой:

  1. получаем баланс пользователя из базы

  2. проверяем хватает ли средств для перевода по полученному балансу

  3. обновляем значения у одного пользователя и другого в базе

Как здесь более правильно защититься от race condition при переводе денег от одного пользователя другому?

Answer 1

В сущности весь указанный вами алгоритм для операции списания не нужен. Достаточно

begin;
update tablename set balance = balance - :amount 
  where user_id = :user and balance >= :amount;
update tablename set balance = balance + :amount
  where user_id = :user_target;
-- прочие действия, из-за двух update выше мы 
-- гарантированно сериализованы для этих двух пользователей
commit;

Необходимо только ловить ситуацию с affected rows = 0. Если первый update ответил, что ничего не обновил - значит или нет пользователя отправителя или у него нет денег. Универсально это не проверить, а говоря о конкретике конкретных диалектов SQL - вполне могут быть способы. Если второй update ничего не изменил - значит не существует получателя. В обоих случаях вам необходимо сделать rollback.

Даже если говорить о полновесной двойной бухгалтерской записи текущий баланс обычно всё равно денормализуют и на его обновлении и можно выстраивать очередь транзакций. Ну а если нет денормализованного баланса - то придётся придумывать другие места для сериализации транзакций. Может быть через advisory locks (если ваша СУБД их предоставляет).

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

begin;
select ... from tablename where ... for update;
-- прочие действия, до завершения этой транзакции 
-- эту строку никто другой изменить не сможет
commit;
Answer 2

Тот, кому нужно перевести, в момент успешного выполнения это будет + к текущему балансу, и не важно какой у него он на тот момент, ведь так?

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

Однако, как заметил idd в комментах может случиться так, что между проверкой баланса и записью во время выполнения скрипта, может произойти другая запись, в таком случае нужно использовать некий счетчик, который позволит определить время работы транзакции и заблокировать на это время выполнение других транзакций.

Начинаем обмен средствами, даем 2 минуты и записываем в базу время окончания плюс некий скажем хеш транзакции (также помещаем его в куки браузера), при выполнении отсылаем хеш с данными где проверяем и если хеш совпадает и время не вышло, записываем, иначе можно оповестить что время истекло или сессия устарела и т.п. В момент когда мы попытаемся создать 2-ой перевод, проверив что уже существует хеш у этого пользователя (он также не совпадает с хешем в браузере) и время не вышло вернем ошибку о том, что необходимо подождать завершения предыдущей транзакции.

READ ALSO
Работа с Bitcoin Core через PHP

Работа с Bitcoin Core через PHP

не могли бы вы подсказать какие-либо материалы, в которых описано взаимодействие с кошельком Bitcoin Core средствами PHP(Необходимо для организации...

231
For в PHP реализация примера [требует правки]

For в PHP реализация примера [требует правки]

Создать массив $ и записать в него 5 произвольных цифрИспользуя цикл for вывести все цифры умноженные на 5

213
Парсинг данных ics и вывод на странице

Парсинг данных ics и вывод на странице

Подскажите как можно спарсить данные из ical - файл https://wwwairbnb

205
Почему не создается архив на сервере?

Почему не создается архив на сервере?

Пытаюсь добавить в архив файлы из указанной директории с помощью стандартного класса ZipArchive, однако архив не создаетсяЧто я делаю не так?...

204