服务器是否会同时处理两个或多个


Will the server handle two or more at the same time?

>我在PHP中有这段代码,将视图写入文本文件并增加数量。我的问题是:如果两个或更多人使用我的网站并运行PHP脚本会发生什么?服务器会处理它吗?增加的内容会保存到文件中吗?

这是我的代码(如果有帮助的话):

<?php
    $clicks = file_get_contents("clicks.txt");
    $clicks++;
        $fp = fopen("clicks.txt", "w+");
        fwrite($fp, $clicks);
        fclose($fp);
//give the count to the user
echo "result: $clicks";
    ?>

首先:这不是我编写点击计数器的方式。

也就是说,100 个用户同时访问您的服务器(初始点击为 0)可能会导致记录的数字为 1..100,其中低(=错误)值突出显示。

  • 如果要在文本文件中计数,请像@lanzz的答案一样锁定,并为重大性能影响做好准备 - 您正在有效地序列化请求。
  • 如果要在任何文件中计数,请考虑 SQlite 并为可管理的性能影响做好准备
  • 如果您只想计数,请考虑一个性能影响非常小的真实数据库

编辑:实现

我为文本文件,SQLite和MySQL中的文件计数器创建了以下实现

请不要因为我使用 mysql_*() 函数系列而激怒我 - 因为代码的全部内容都是指导性的,而不是富有成效的:在专注于手头的问题而不是周围层的意义上具有指导性。

计数器文件.php:

<?php
//acquire file handle
$fd=fopen('counter.txt','c+b');
if (!$fd) die("Can't acquire file handle");
//lock the file - we must do this BEFORE reading, as not to read an outdated value
if (!flock($fd,LOCK_EX)) die("Can't lock file");
//read and sanitize the counter value
$counter=fgets($fd,10);
if ($counter===false) die("Can't read file");
if (!is_numeric($counter)) {
    flock($fd,LOCK_UN);
    die("Value in file '$counter' is not numeric");
}
//increase counter and reconvert to string
$counter++;
$counter="$counter";
//Write to file
if (!rewind($fd)) {
    flock($fd,LOCK_UN);
    die("Can't rewind file handle");
}
$num=fwrite($fd,$counter);
if ($num!=strlen($counter)) {
    flock($fd,LOCK_UN);
    die("Error writing file");
}
//Unlock the file and close file handle
flock($fd,LOCK_UN);
fclose($fd);
printf ("Counter is now %05d",$counter);
?>

Counter-SQLite.php:

<?php
//counter.sqlite3 was created with 
//CREATE TABLE counter (counter NUMERIC)
//INSERT INTO counter VALUES (0)
//Open database
$dsn='sqlite:'.dirname(__FILE__).'/counter.sqlite3';
$db=new PDO($dsn);
if (!$db) die("Can't open SQlite database via DBO");
//Make exclusive
$sql="BEGIN EXCLUSIVE TRANSACTION";
if ($db->exec($sql)===false) die("Error starting exclusive transaction");
//Update counter
$sql="UPDATE counter SET counter=counter+1";
if (!$db->exec($sql)) die("Error inserting into database");
//Read value
$sql="SELECT counter FROM counter";
$result=$db->query($sql);
if (!$result) die("Error querying database");
foreach ($result as $row) $counter=$row['counter'];
//Commit
$sql="COMMIT TRANSACTION";
if (!$db->exec($sql)) die("Error committing to database");
//Print result
printf("Counter is now %05d",$counter);
?>

Counter-mysql.php:

<?php
//mysql database was created with 
//CREATE TABLE counter (counter INT NOT NULL)
//INSERT INTO counter VALUES (0)
//Open database connection and select database 
$db=mysql_pconnect('127.0.0.1','redacted','redacted');
if (!$db) die("Can't open database");
if (!mysql_select_db('redacted', $db)) die("Can't select database");
//Update counter
$sql="UPDATE counter SET counter=counter+1";
$qry=mysql_query($sql,$db);
if (!$qry) die("Error updating database");
//Read value
$sql="SELECT counter FROM counter";
$qry=mysql_query($sql,$db);
if (!$qry) die("Error reading from database");
$counter=mysql_fetch_array($qry,MYSQL_ASSOC);
if (!$counter) die("Error reading result");
//Print result
printf("Counter is now %05d",$counter['counter']);
?>

至于表现:我站正了。SQLite 实现比其他两个慢 100 倍 - 这是因为我不得不接受,除了START EXCLUSIVE TRANSACTION之外,没有什么可以结束 1000 次点击的ab -n 1000 -c 50 http://127.0.0.1/stackoverflow/counter/counter-sqlite.php测试。

我对 OP 的建议是使用 MySQL 版本 - 它速度很快,并且可以在操作系统崩溃时可靠地保存计数器。文件版本具有几乎相同的性能特征,但它很容易作系统崩溃破坏。

服务器无法自行处理它。您需要自己实现文件锁定。参考。

您询问的行为取决于文件系统如何处理文件 I/O,而不是 PHP 本身。命令行脚本将有同样的问题(如果其中许多可以同时运行)。

您的代码受两个争用条件的约束:文件的磁盘内容可能会在您读取文件和打开文件进行写入之间更改;并且可能会在打开写入和实际写入(刷新)到磁盘之间更改。

第一个争用条件意味着您将写入一个过时的值:您从 10 增加到 11,但另一个线程已经从 10 增加到 11。您失去了一次点击。第二个争用条件更微妙:由于w+模式截断文件,如果另一个线程在之后尝试读取,它将读取一个空文件,并且可能认为计数为零。最后,您的文件内容可能会损坏。请考虑以下事件序列:

    线程
  1. 1 和线程 2 都从文件中读取"8",然后打开它进行写入(截断它)。
  2. 线程
  3. 1 写入"9",但线程 2 延迟几毫秒。
  4. 同时,线程 3 打开文件,读取"9",然后写入"10"。
  5. 最后,线程 2 唤醒,将"9"写入文件开头并退出。

结果:文件现在显示"90"。每当同时写入不同长度的数据时,这是一个真正的问题。

结论:不要在磁盘文件中维护状态,除非可以合理地预期不会有并发访问。如果游客很少而且相距很远,没问题。但是,如果您的猫视频传播开来,您的计数器将被打乱,您必须要求您的父母检查他们的服务器日志并计算您页面的点击量。

请改用数据库:数据库旨在处理并发性。安排对MySQL数据库的访问(它不需要您的父母授予您在服务器上的重要权限)。甚至不用这样做,您可以通过在计算机上安装供私人使用的数据库来学习数据库。如果你有一台Windows电脑,easyPHP是一种快速的方法,通过一次快速下载来帮助自己使用私人网络服务器,php和MySQL。

下面的代码将实现您的代码,但具有文件锁定功能。当文件无法锁定时,它将等待半秒钟,然后重试。

<?php
    $clicks = file_get_contents("clicks.txt");
    $clicks++;
    $fp = fopen("clicks.txt", "w+");
    while ( !flock($fp, LOCK_EX) ) {    
        usleep(500000); // Delay half a second
    }
    fwrite($fp, $clicks);
    fclose($fp);
    flock($fp, LOCK_UN);
    //give the count to the user
    echo "result: $clicks";
?>

在您的示例中,您可能有两个锁定问题:

  • 点击次数可以重置为 0
  • 点击
  • 可以通过另一个点击来擦除

您可以使用 flock 方法 (http://php.net/manual/en/function.flock.php) 锁定文件。如果锁定失败,则需要重试。

AFAIK 如果脚本同时执行,那么它不会给你两次点击,而只会给你一次点击。最好使用数据库进行此操作。

实际上,如果您预计网站上的流量较低,那么此脚本将做得很好。但它会在更高的负载下失效。

大多数人会不同意这样的方法,但是你会在并发请求时遇到问题。作为解决方案,您可以使用独占文件锁定。

查看示例: http://www.php.net/manual/en/function.flock.php

我认为避免冲突的最佳方法是使用日志系统。你可以试试这个日志库:http://pear.php.net/package/Log/docs/latest/Log/Log.html