Я разрабатываю Интернет-банкинг, работает на Google App Engine и разработан в PHP.
Состояние гонки — большая проблема.
У пользователей никогда не может быть меньше 0,00 долларов (я знаю, что у меня есть другие проблемы, о которых стоит беспокоиться, но давайте сосредоточимся только на этом прямо сейчас).
Псевдо-код:
<?php
$user_id = $_GET['user_id'];
$withdraw_amount = $_GET['withdraw_amount'];
if(getUserBalance($user_id) - $withdraw_amount >= 0){
setUserBalance($user_id, getUserBalance($user_id) - $withdraw_amount);
sendMoneyToUser($user_id, $withdraw_amount);
echo 'Success';
}
else{
echo 'Insufficient funds';
}
?>
База данных Google Cloud SQL:
USER_ID BALANCE
1 10.00
2 20.00
* Приведенная выше база данных является очень упрощенной версией фактической базы данных. Таблицы / строки блокировки SQL не будут работать для меня в реальном мире. Но если у вас есть другое решение, использующее SQL вместо PHP, я хочу знать об этом.
Случай гонки:
GAE instance #1 GAE instance #2 (SAME TIME)
user_id = '1' user_id = '1'
withdraw_amount = 10.00 withdraw_amount = 10.00
balance = getUserBalance('1') balance = getUserBalance('1')
//balance = 10.00 //balance = 10.00
10.00 - 10.00 >= 0 -> (true) 10.00 - 10.00 >= 0 -> (true)
setUserBalance ('1', 0.00) setUserBalance ('1', 0.00)
sendMoneyToUser('1', 10.00) sendMoneyToUser('1', 10.00)
User gets $10.00 User gets $10.00
The user now has $20.00 in his hands, but he had just $10.00 in the bank!!!
Как я могу предотвратить условия гонки?
Помните, что код работает на Google App Engine и у меня нет доступа к Семафоры PHP расширение.
Одним из способов будет использование библиотеки мьютексов, которая использует что-то вроде redis
в качестве бэкэнда, чтобы вы могли без проблем вызывать его с разных машин.
Получите блокировку, специфичную для пользователя, до начала операции, выполните свою работу и затем снимите блокировку. Пока вы выполняете работу, последующие вызовы для выполнения операции не будут выполнены, поскольку они не могут получить блокировку.
В псевдокоде это будет так просто:
$lockName = "some string that contains the user/customer ID in it to be unique per customer";
if ( ! $mutex->acquireLock($lockName, 60) ) {
throw new Exception ("An operation is already running, please wait for it to finish and try again!");
}
// do the transaction data here, substract the amount, etc.
// once done, release the lock:
$mutex->releaseLock($lockName);
Других решений пока нет …