Если у меня есть сценарий, который уменьшает баланс пользователей в системе двойного учета, и злонамеренный пользователь решает выполнить этот сценарий на своем аккаунте на двух разных компьютерах (или на одном и том же компьютере) ровно в одно и то же время, все это будет бежать дважды, верно? Так вот мой упрощенный гипотетический сценарий.
$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, будет иметь огромные проблемы. Вы не можете сделать это.
Вместо этого возьмите шаблон set / test / fail. Попробуйте и вычтите, используя атомарный оператор 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
где вы можете применить условия к подзапросу для возврата нулевых строк в случае недостаточного баланса.
Других решений пока нет …