Самый надежный и безопасный метод предотвращения условий гонки в PHP
Мне нужно использовать мьютексы или семафоры в PHP, и это меня пугает. Чтобы уточнить, я не боюсь писать код без взаимоблокировок, который правильно синхронизируется, или боюсь опасностей параллельного программирования, но того, насколько хорошо PHP обрабатывает дополнительные случаи.
Краткая справочная информация: написание интерфейса обработчика кредитных карт, который находится между пользователями и сторонним шлюзом кредитных карт. Необходимо предотвратить дублирование запросов, и у вас уже есть система, которая работает, но если пользователь попадет отправка (без JS включена, поэтому я не могу отключить кнопку для них) с интервалом в миллисекунды возникает состояние гонки, когда мой PHP-скрипт не осознает, что был сделан дублирующий запрос. Нужен семафор/мьютекс, чтобы я мог гарантировать, что для каждой уникальной транзакции проходит только один успешный запрос.
Я запускаю PHP за nginx через PHP-FPM с несколькими процессами на многоядерной машине Linux. Я хочу быть уверен, что
- семафоры являются общими для всех процессов php-fpm и по всем ядрам (ядро i686).
- php-fpm обрабатывает сбой процесса PHP, удерживая мьютекс/семафор, и соответственно освобождает его.
- php-fpm обрабатывает прерывание сеанса, удерживая мьютекс/семафор, и соответственно освобождает его.
Да, я знаю. Очень простые вопросы, и было бы глупо думать, что для любого другого программного обеспечения не существует правильного решения. Но это PHP, и он, безусловно, не был построен с учетом параллелизма, он часто выходит из строя (в зависимости от того, какие расширения вы загрузили), и находится в нестабильной среде (PHP-FPM и в Интернете).
Что касается (1), я предполагаю, что если PHP использует функции POSIX, то оба эти условия выполняются на машине SMP i686. Что касается (2), я вижу из краткого просмотра документов, что есть параметр, который определяет это поведение (хотя почему кто-то когда-либо хотел, чтобы PHP НЕ выпускал мьютекс, если сеанс убит, я не понимаю). Но (3) является моей главной заботой, и я не знайте, можно ли с уверенностью предположить, что php-fpm правильно обрабатывает все дополнительные случаи для меня. Я (очевидно) никогда не хочу тупика, но я не уверен, что могу доверять PHP, чтобы он никогда не оставлял мой код в состоянии, когда он не может получить мьютекс, потому что сеанс, который его захватил, был либо изящно, либо нелюбезно завершен.
Я рассматривал возможность использования подхода MySQL LOCK TABLES
, но там еще больше сомнений, потому что, хотя я доверяю блокировке MySQL больше, чем блокировке PHP, я боюсь, что PHP прервет запрос (с*выходом* сбоем) удерживая блокировку сеанса MySQL, MySQL может удерживать таблицу заблокированной (особенно потому, что я легко могу представить код, который приведет к этому).
Честно говоря, мне было бы удобнее всего использовать очень простое расширение C, где я мог бы точно видеть, какие вызовы POSIX выполняются и с какими параметрами, чтобы обеспечить точное поведение, которое я хочу.. но я не с нетерпением жду написания этого кода.
У кого-нибудь есть какие-либо рекомендации, связанные с параллелизмом, в отношении PHP, которые они могли бы хотите поделиться?
5 answers
На самом деле, я думаю, что нет необходимости в сложном мьютексе/семафоре, независимо от решения.
Прочитав ответ dqhendricks и проведя некоторые исследования, я обнаружил, что ключи формы через $_SESSION
- это все, что вам нужно. В PHP сеансы блокируются и session_start()
ожидает, пока сеанс пользователя не будет освобожден. Вам просто нужно unset()
ввести ключ формы при первом действительном запросе. Второй запрос должен подождать, пока первый не завершит сеанс.
Однако при запуске в (не сеансе или исходном ip-адресе на основе) сценария балансировки нагрузки все становится сложнее. Для такого сценария, я уверен, вы найдете ценное решение в этой замечательной статье: http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/
Я воспроизвел ваш вариант использования со следующей демонстрацией. просто забросьте этот файл на свой веб-сервер и протестируйте его:
<?php
session_start();
if (isset($_REQUEST['do_stuff'])) {
// do stuff
if ($_REQUEST['uniquehash'] == $_SESSION['uniquehash']) {
echo "valid, doing stuff now ... "; flush();
// delete formkey from session
unset($_SESSION['uniquehash']);
// release session early - after committing the session data is read-only
session_write_close();
sleep(20);
echo "stuff done!";
}
else {
echo "nope, {$_REQUEST['uniquehash']} is invalid.";
}
}
else {
// show form with formkey
$_SESSION['uniquehash'] = md5("foo".microtime().rand(1,999999));
?>
<html>
<head><title>session race condition example</title></head>
<body>
<form method="POST">
<input type="hidden" name="PHPSESSID" value="<?=session_id()?>">
<input type="text" name="uniquehash"
value="<?= $_SESSION['uniquehash'] ?>">
<input type="submit" name="do_stuff" value="Do stuff!">
</form>
</body>
</html>
<?php } ?>
Вы можете сохранить случайный хэш в массиве в данных сеанса, а также распечатать этот хэш в виде скрытого значения ввода формы. при поступлении запроса, если скрытое значение хэша существует в вашем массиве сеансов, вы можете удалить хэш из сеанса и обработать форму, в противном случае не делайте этого.
Это должно предотвратить повторную отправку форм, а также помочь предотвратить атаки csrf.
У вас есть интересный вопрос, но у вас нет никаких данных или кода для отображения.
В 80% случаев вероятность того, что что-то неприятное произойдет из-за самого PHP, практически равна нулю, если вы будете следовать стандартным процедурам и методам, касающимся многократной отправки пользователями форм, что применимо почти ко всем другим настройкам, а не только к PHP.
Если вы являетесь 20% и ваша среда требует этого, то одним из вариантов является использование очередей сообщений, в чем я уверен знакомый с. Опять же, эта идея является языковой агностикой. Ничего общего с языками. Все дело в том, как перемещаются данные.
Если проблема возникает только при нажатии кнопки с интервалом в миллисекунды, не сработает ли программный деактиватор? Например, экономить время нажатия кнопки в переменной сеанса и не допускать больше, скажем, секунды? Просто идея перед моим утренним кофе. Ваше здоровье.
Что я делаю, чтобы предотвратить состояние гонки сеанса в коде, так это после последней операции, которая сохраняет данные в сеансе, я использую функцию PHPsession_write_close() обратите внимание, что если вы используете PHP 7, вам нужно отключить буферизацию вывода по умолчанию в php.ini. Если у вас есть трудоемкие операции, было бы лучше выполнить их после вызова session_write_close().
Я надеюсь, что это кому-то поможет, для меня это спасло мою жизнь:)