Если у меня есть скрипт, уменьшающий баланс пользователя в системе двойной записи, и злоумышленник решит выполнить этот скрипт на своей учетной записи на двух разных машинах (или на одной машине) в ТОЧНО одно и то же время, все это будет запустить дважды, верно? Итак, вот мой упрощенный гипотетический сценарий.
$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, оставив отрицательное значение в бухгалтерской книге?
Как бы вы предотвратили подобное? Жизненно важно поддерживать целостность данных в этой таблице базы данных.
Тогда транзакции базы данных? Так просто? Было бы здорово, если бы это было так. Я был обеспокоен тем, что потребуются другие проверки целостности, в которых я не был уверен.
В некотором смысле это просто подтвердило и развеяло мою тревогу по поводу этого кода. Я не доверяю своим собственным исследованиям, чтобы предположить, что это все, что нужно, чтобы сохранить записи в безопасности!
есть блокировка без транзакций и различные типы блокировки, но да, вы поняли.






Вы говорите о условия гонки, и их очень сильно важно исключить из финансового кодекса.
Все, что следует шаблону 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 simonw16, если вы не можете разумно включить необходимую логику в один запрос, вам действительно следует использовать формальную транзакцию и SELECT ... FOR UPDATE для блокировки необходимых строк на время транзакции.
@Sammitch Здесь применяются обычные предостережения о блокировках. Если ваш код умирает в неудобном месте и не освобождает блокировку, вы можете создать огромный беспорядок. Всегда проверяйте этот код под нагрузкой перед запуском.
@tadman IIRC, если соединение теряется в середине соединения, это неявный откат. Ваш код должен был бы умереть особенно неприятным образом и держать соединение открытым, чтобы привести к такой ситуации. Даже тогда я бы сказал, что это предпочтительнее, чем непреднамеренное копирование или уничтожение валюты.
так как это система бухгалтерского учета с двойной записью, мы не обновляем ее. На самом деле у нас есть триггеры для предотвращения UPDATE и DELETE, разрешающие только INSERT. Можно ли их заблокировать таким же образом или будет заблокирована вся таблица. Означает ли это, что одновременным пользователям придется ждать, пока транзакционный пользователь завершит... транзакцию?
@simonw16 Уф. Итак, вы получили таблицу, которая представляет собой книгу кредитных и дебетовых транзакций? Мое предложение состояло бы в том, чтобы заблокировать всю таблицу бухгалтерской книги и любые связанные строки баланса на время транзакции SQL. Но, вероятно, более целесообразным было бы обратиться к ресурсу, посвященному проектированию финансовых систем, будь то книга, известный проект с открытым исходным кодом или человек.
@ Sammitch Хммм, да, это меня беспокоит. Блокировка всей таблицы в системе, которую необходимо масштабировать для нескольких тысяч пользователей. Одновременно несколько сотен. Это тоже может вызвать проблемы. Интересно, как это достигается до сих пор. Я мог бы взять книгу с Amazon, я полагаю.
@simonw Я не думаю, что вы можете масштабировать электронную книгу по той же причине, по которой вы не можете иметь более одного человека, пишущего в бумажной книге. Существуют строгие ограничения на порядок операций, которые нельзя мультиплексировать.
Тогда, возможно, нетранзакционный способ блокировки. Возможно, что @tadman говорит с запросом на обновление.
заблокировать строку, проверить строку, выполнить математику в строке, разблокировать строку