Самый надежный и безопасный метод предотвращения условий гонки в PHP


Мне нужно использовать мьютексы или семафоры в PHP, и это меня пугает. Чтобы уточнить, я не боюсь писать код без взаимоблокировок, который правильно синхронизируется, или боюсь опасностей параллельного программирования, но того, насколько хорошо PHP обрабатывает дополнительные случаи.

Краткая справочная информация: написание интерфейса обработчика кредитных карт, который находится между пользователями и сторонним шлюзом кредитных карт. Необходимо предотвратить дублирование запросов, и у вас уже есть система, которая работает, но если пользователь попадет отправка (без JS включена, поэтому я не могу отключить кнопку для них) с интервалом в миллисекунды возникает состояние гонки, когда мой PHP-скрипт не осознает, что был сделан дублирующий запрос. Нужен семафор/мьютекс, чтобы я мог гарантировать, что для каждой уникальной транзакции проходит только один успешный запрос.

Я запускаю PHP за nginx через PHP-FPM с несколькими процессами на многоядерной машине Linux. Я хочу быть уверен, что

  1. семафоры являются общими для всех процессов php-fpm и по всем ядрам (ядро i686).
  2. php-fpm обрабатывает сбой процесса PHP, удерживая мьютекс/семафор, и соответственно освобождает его.
  3. 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, которые они могли бы хотите поделиться?

Author: Mahmoud Al-Qudsi, 2012-02-15

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 } ?>
 9
Author: Kaii, 2012-02-18 13:30:49

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

Это должно предотвратить повторную отправку форм, а также помочь предотвратить атаки csrf.

 1
Author: dqhendricks, 2012-02-15 07:27:21

У вас есть интересный вопрос, но у вас нет никаких данных или кода для отображения.

В 80% случаев вероятность того, что что-то неприятное произойдет из-за самого PHP, практически равна нулю, если вы будете следовать стандартным процедурам и методам, касающимся многократной отправки пользователями форм, что применимо почти ко всем другим настройкам, а не только к PHP.

Если вы являетесь 20% и ваша среда требует этого, то одним из вариантов является использование очередей сообщений, в чем я уверен знакомый с. Опять же, эта идея является языковой агностикой. Ничего общего с языками. Все дело в том, как перемещаются данные.

 1
Author: zaf, 2012-02-15 09:06:54

Если проблема возникает только при нажатии кнопки с интервалом в миллисекунды, не сработает ли программный деактиватор? Например, экономить время нажатия кнопки в переменной сеанса и не допускать больше, скажем, секунды? Просто идея перед моим утренним кофе. Ваше здоровье.

 0
Author: tommyo, 2012-02-15 07:20:30

Что я делаю, чтобы предотвратить состояние гонки сеанса в коде, так это после последней операции, которая сохраняет данные в сеансе, я использую функцию PHPsession_write_close() обратите внимание, что если вы используете PHP 7, вам нужно отключить буферизацию вывода по умолчанию в php.ini. Если у вас есть трудоемкие операции, было бы лучше выполнить их после вызова session_write_close().

Я надеюсь, что это кому-то поможет, для меня это спасло мою жизнь:)

 0
Author: Robert, 2017-12-21 23:54:07