防止PHP中竞争条件的最可靠和安全的方法

2022-08-30 17:15:36

我需要在PHP中使用互斥体或信号量,这让我感到害怕。为了澄清,我并不害怕编写正确同步的无死锁代码,也不害怕并发编程的危险,而是害怕PHP处理边缘情况的能力。

快速背景:编写位于用户和第三方信用卡网关之间的信用卡处理程序接口。需要防止重复的请求,并且已经有一个工作正常的系统,但是如果用户点击提交(没有启用JS,所以我不能为他们禁用按钮)相隔几毫秒,则会出现争用情况,我的PHP脚本没有意识到已经发出了重复的请求。需要一个信号量/互斥体,这样我就可以确保每个唯一事务只有一个成功的请求通过。

我正在通过PHP-FPM在nginx后面运行PHP,在多核Linux机器上运行多个进程。我想确保

  1. 信号量在所有php-fpm进程和所有内核(i686内核)之间共享。
  2. php-fpm 在持有互斥锁/信号量的同时处理 PHP 进程崩溃,并相应地释放它。
  3. php-fpm 在持有互斥锁/信号量的同时处理会话中止,并相应地释放它。

是的,我明白。非常基本的问题,认为任何其他软件都不存在适当的解决方案是愚蠢的。但这是PHP,它肯定不是在考虑并发的情况下构建的,它经常崩溃(取决于你加载了哪些扩展),并且处于不稳定的环境(PHP-FPM和Web上)。

关于(1),我假设如果PHP使用POSIX函数,那么这两个条件在SMP i686机器上都成立。至于(2),我从简要浏览文档中看到有一个参数决定了这种行为(尽管为什么有人希望PHP不发布互斥体是会话被杀死,我不明白)。但是(3)是我主要关心的问题,我不知道是否可以安全地假设php-fpm为我正确处理所有边缘情况。我(显然)从来不想要死锁,但我不确定我是否可以相信PHP永远不会让我的代码处于无法获得互斥锁的状态,因为抓住它的会话要么优雅地终止,要么不优雅地终止。

我已经考虑过使用MySQL方法,但还有更多的疑问,因为虽然我更信任MySQL锁而不是PHP锁,但我担心如果PHP在持有MySQL会话锁时中止请求(*out*崩溃),MySQL可能会保持表锁定(特别是因为我可以很容易地想象导致这种情况发生的代码)。LOCK TABLES

老实说,我最满意的是一个非常基本的C扩展,在那里我可以确切地看到POSIX调用的内容以及使用哪些参数来确保我想要的确切行为。但我不期待编写该代码。

任何人都有任何关于PHP的并发相关最佳实践,他们想分享吗?


答案 1

实际上,我认为不需要复杂的互斥体/信号量的任何解决方案。

存储在 PHP 中的表单密钥就是您所需要的。作为一个不错的副作用,此方法还可以保护您的表单免受CSRF攻击。$_SESSION

在 PHP 中,会话通过获取 POSIX 来锁定,PHP 等待直到用户会话被释放。您只需要在第一个有效请求上使用表单密钥即可。第二个请求必须等到第一个请求释放会话。flock()session_start()unset()

但是,当在涉及多个主机的负载平衡方案(不是基于会话或源 IP)中运行时,事情变得越来越复杂。对于这种情况,我相信您会在这篇伟大的论文中找到有价值的解决方案:http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

我通过以下演示复制了您的用例。只需将此文件放入您的Web服务器并对其进行测试即可:

<?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 } ?>

答案 2

您有一个有趣的问题,但您没有任何数据或代码可以显示。

对于80%的情况下,如果您遵循有关阻止用户多次提交表单的标准程序和做法,则由于PHP本身而发生任何令人讨厌的事情的可能性几乎为零,这几乎适用于所有其他设置,而不仅仅是PHP。

如果你是20%,而你的环境需要它,那么一种选择是使用消息队列,我相信你很熟悉。同样,这个想法与语言无关。与语言无关。它完全是关于数据如何移动的。


推荐