Предотвращение двойного исполнения. PHP финансовое приложение

Если у меня есть скрипт, уменьшающий баланс пользователя в системе двойной записи, и злоумышленник решит выполнить этот скрипт на своей учетной записи на двух разных машинах (или на одной машине) в ТОЧНО одно и то же время, все это будет запустить дважды, верно? Итак, вот мой упрощенный гипотетический сценарий.

    $balance = $user->ledger->getBalance(); // returns 5000
    $amount = 3000;
    if ($amount <= $balance) {
        $user->ledger->decrease($amount);
    }

    echo $user->ledger->getBalance(); // echo's 2000

Если скрипт запускать один за другим, то второе выполнение завершится ошибкой, потому что на счету осталось всего 2000, а он пытался уменьшиться на 3000.

Если сценарии запускаются одновременно и в одно и то же время, не будут ли оба баланса равны 5000, а выполнение обоих сценариев вычтет 3000, оставив отрицательное значение в бухгалтерской книге?

Как бы вы предотвратили подобное? Жизненно важно поддерживать целостность данных в этой таблице базы данных.

заблокировать строку, проверить строку, выполнить математику в строке, разблокировать строку

user10051234 08.02.2019 01:35

Тогда транзакции базы данных? Так просто? Было бы здорово, если бы это было так. Я был обеспокоен тем, что потребуются другие проверки целостности, в которых я не был уверен.

simonw16 08.02.2019 01:35

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

simonw16 08.02.2019 01:37

есть блокировка без транзакций и различные типы блокировки, но да, вы поняли.

user10051234 08.02.2019 01:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
5
149
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы говорите о условия гонки, и их очень сильно важно исключить из финансового кодекса.

Все, что следует шаблону get/test/set, будет иметь огромные проблемы. Вы не можете этого сделать.

Вместо этого используйте шаблон «установил/проверил/не прошел». Попробуйте сделать вывод с помощью атомарного оператора SQL, такого как отдельная операция или блок транзакции. Если это сдвинет баланс в минус, откатите его обратно.

Например, это плохой:

balance = query("SELECT balance FROM accounts WHERE account_id=?")
balance -= amount
balance = query("UPDATE accounts SET balance=?")

Между выборкой и записью может произойти что угодно.

Вместо этого вы могли бы сделать это, когда этот запрос либо завершается успешно, либо завершается ошибкой, его нельзя прервать:

query("UPDATE accounts SET balance=balance-? WHERE account_id=? AND balance>?")

Этот запрос не будет выполняться, если не останется достаточного баланса. В результате вы получите нулевые измененные строки.

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

Существует множество способов структурирования этих операторов INSERT, чтобы сделать невозможным возникновение отрицательного баланса, например INSERT INTO x SELECT ... FROM y, где вы можете применять условия к подзапросу, чтобы возвращать нулевые строки в случае недостаточного баланса.

Хороший исчерпывающий ответ :) У меня было это в голове, по крайней мере, приблизительное приближение к транзакциям, но, честно говоря, мне нужно было какое-то подтверждение моего решения. Мне тоже нравится подход INSERT INTO x SELECT ... FROM y, но у меня есть доступ к транзакциям, так что это будет выход!

simonw16 08.02.2019 01:50

@ simonw16 simonw16, если вы не можете разумно включить необходимую логику в один запрос, вам действительно следует использовать формальную транзакцию и SELECT ... FOR UPDATE для блокировки необходимых строк на время транзакции.

Sammitch 08.02.2019 01:52

@Sammitch Здесь применяются обычные предостережения о блокировках. Если ваш код умирает в неудобном месте и не освобождает блокировку, вы можете создать огромный беспорядок. Всегда проверяйте этот код под нагрузкой перед запуском.

tadman 08.02.2019 01:54

@tadman IIRC, если соединение теряется в середине соединения, это неявный откат. Ваш код должен был бы умереть особенно неприятным образом и держать соединение открытым, чтобы привести к такой ситуации. Даже тогда я бы сказал, что это предпочтительнее, чем непреднамеренное копирование или уничтожение валюты.

Sammitch 08.02.2019 02:12

так как это система бухгалтерского учета с двойной записью, мы не обновляем ее. На самом деле у нас есть триггеры для предотвращения UPDATE и DELETE, разрешающие только INSERT. Можно ли их заблокировать таким же образом или будет заблокирована вся таблица. Означает ли это, что одновременным пользователям придется ждать, пока транзакционный пользователь завершит... транзакцию?

simonw16 08.02.2019 02:12

@simonw16 Уф. Итак, вы получили таблицу, которая представляет собой книгу кредитных и дебетовых транзакций? Мое предложение состояло бы в том, чтобы заблокировать всю таблицу бухгалтерской книги и любые связанные строки баланса на время транзакции SQL. Но, вероятно, более целесообразным было бы обратиться к ресурсу, посвященному проектированию финансовых систем, будь то книга, известный проект с открытым исходным кодом или человек.

Sammitch 08.02.2019 02:20

@ Sammitch Хммм, да, это меня беспокоит. Блокировка всей таблицы в системе, которую необходимо масштабировать для нескольких тысяч пользователей. Одновременно несколько сотен. Это тоже может вызвать проблемы. Интересно, как это достигается до сих пор. Я мог бы взять книгу с Amazon, я полагаю.

simonw16 08.02.2019 02:29

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

Sammitch 08.02.2019 03:36

Тогда, возможно, нетранзакционный способ блокировки. Возможно, что @tadman говорит с запросом на обновление.

simonw16 08.02.2019 05:19

Другие вопросы по теме