我需要在PHP中使用互斥或信号量,这让我很害怕。为了澄清,我不怕写正确同步的无死锁代码,也不怕并发编程的危险,而是害怕PHP处理边缘情况的能力。
快速背景:编写一个位于用户和第三方信用卡网关之间的信用卡处理程序界面。需要防止重复请求,并且已经有了一个可以工作的系统,但如果用户每隔几毫秒点击提交(启用了JS,所以我不能为他们禁用按钮),就会出现竞争条件,我的PHP脚本没有意识到已经发出了重复请求。需要一个信号量/互斥量,这样我就可以确保每个唯一事务只通过一个成功的请求。
我通过PHP-FPM在nginx后面运行PHP,在多核Linux机器上有多个进程。我想确定
- 信号量在所有php-fpm进程之间以及在所有内核(i686内核)之间共享
- php-fpm在持有互斥/信号量时处理php进程崩溃,并相应地释放它
- php-fpm在持有互斥/信号量时处理会话中止,并相应地释放它
是的,我知道。非常基本的问题,如果认为任何其他软件都不存在合适的解决方案,那将是愚蠢的。但这是PHP,它的构建肯定没有考虑到并发性,它经常崩溃(取决于您加载的扩展),并且处于不稳定的环境中(PHP-FPM和web上)。
关于(1),我假设如果PHP使用POSIX函数,那么这两个条件在SMP i686机器上都成立。至于(2),我从简要浏览文档中看到,有一个参数决定了这种行为(尽管我不明白为什么PHP不释放互斥体是因为会话被终止了)。但是(3)是我主要关心的问题,我不知道假设php-fpm为我正确地处理所有边缘情况是否安全。我(显然)从不希望出现死锁,但我不确定我是否可以相信php永远不会让我的代码处于无法获得互斥的状态,因为获取它的会话要么被优雅地终止,要么被不礼貌地终止。
我曾考虑过使用MySQL LOCK TABLES
方法,但还有更多的疑问,因为虽然我更信任MySQL锁而不是PHP锁,但我担心如果PHP在持有MySQL会话锁时中止请求(*out*崩溃),MySQL可能会锁定表(尤其是因为我很容易想象会导致这种情况发生的代码)。
老实说,我最喜欢的是一个非常基本的C扩展,在那里我可以准确地看到正在进行的POSIX调用,以及使用什么参数来确保我想要的确切行为。。但我并不期待写那个代码。
有人想分享关于PHP的任何与并发相关的最佳实践吗?
事实上,我认为无论什么解决方案,都不需要复杂的互斥体/信号量。
存储在PHP $_SESSION
中的表单键就是您所需要的全部。作为一个很好的副作用,这种方法还可以保护你的形态免受CSRF的攻击。
在PHP中,通过获取POSIX flock()
来锁定会话,而PHP的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 } ?>
这是一个有趣的问题,但您没有任何数据或代码可以显示。
在80%的情况下,如果你遵循关于阻止用户多次提交表单的标准程序和实践,那么由于PHP本身而发生任何恶劣事件的可能性几乎为零,这几乎适用于其他所有设置,而不仅仅是PHP。
如果你是20%,并且你的环境需要它,那么一种选择是使用消息队列,我相信你很熟悉。同样,这种观点是语言不可知论的。与语言无关。这一切都与数据如何移动有关。
您可以将随机哈希存储在会话数据中的数组中,也可以将该哈希打印为隐藏的表单输入值。当收到请求时,如果会话数组中存在隐藏的哈希值,则可以从会话中删除该哈希并处理表单,否则不处理。
这应该可以防止重复的表单提交,并有助于防止csrf攻击。
如果问题只出现在相隔几毫秒按下一个按钮时,软件解除绑定不起作用吗?比如在会话变量中节省按下按钮的时间,而不允许再按下一秒钟?就在我早上喝咖啡之前。干杯
为了防止代码中的会话竞争条件,我所做的是在会话中存储数据的最后一次操作之后。我使用PHP函数session_write_close()注意,如果您使用的是PHP 7,则需要在PHP.ini中禁用默认输出缓冲。如果您有耗时的操作,最好在调用session_write_close(。
我希望它能帮助别人,对我来说它救了我的命:)