0

我写了这个计数器,但是在每小时大约 100-200 次唯一点击的负载下存在一个奇怪的问题。计数重置为大约“120500”的奇怪值,并从那里继续计数,直到稍后重置为相同的值。现在,我会理解一个零值,但是 120500 来自哪里?这是完整的计数器代码:

<?php

    class Counter {
        public $currentCount;
        private $countFile;

        public function __construct($file) {
            if(!file_exists($file)) {
                $fp = fopen($file, 'w');
                fwrite($fp, '1');
                fclose($fp);
            }
            $this->countFile = $file;
            $this->currentCount = file_get_contents($this->countFile);
        }

        public function incrementPerSession() {
            if(isset($_SESSION['visitWritten'])) {
                echo $this->currentCount;
            } else {
                $count = $this->currentCount + 1;
                $this->writeNewCount($count);
                echo $count;
            }
        }

        private function writeNewCount($count) {
            $delay = rand(10000, 80000);
            $fp = fopen($this->countFile, 'w');
            if(flock($fp, LOCK_EX)) {   // PHP locks are not reliable.
                usleep($delay);         // usleep() works as a workaround and prevents resets to zero
                fwrite($fp, $count);
                flock($fp, LOCK_UN);
                $_SESSION['visitWritten'] = true;
            } else {
                echo 'Counter: Could not write to file';
            }
            fclose($fp);
        }
    }
?>

这只是偶尔发生,我认为这与同时写入有关。会话变量“visitWritten”未在站点的其他任何地方使用。我怎样才能提高这门课?

4

1 回答 1

1

我不知道 ~120500 这个数字是从哪里来的。我只是要解决这里的比赛条件。

需要注意的几点:

  • Linux 中的文件锁定是建议性的* — 一切都必须表现得很好才能相处,并且表现得很好是一种选择加入而不是选择退出的事情。
  • file_get_contents()玩得不好。如果文件被锁定,哦,好吧——无论如何它都会被读取。该功能甚至不知道那里有锁。
  • fopen($file, w)在锁定文件之前立即截断文件。

*它可以通过文件系统挂载选项强制执行,但我从未见过它在使用中。

那么该怎么办。让我们从写作开始。我们的想法是延迟截断文件,直到我们锁定它之后。

写:

$fh = fopen($file, a+); // Open the file in append-or-create mode.
                        // You may as well use the same function for
                        // creating the file as you do to update it.

if (flock($fh, LOCK_EX)) {
    rewind($fh);        // Set the file pointer to the start of the file.

    ftruncate($fh, 0);  // Truncate the file. This is safe since we've got a lock.
                        // Or will be safe, once we make everything respect locks.

    fwrite($fh, $string);
    flock($fh, LOCK_UN);
}
fclose($fh);

现在,即使在我们打开文件并锁定文件之间读取文件,他们也会得到数据,因为我们还没有截断它。我们要做的就是让文件读取以尊重锁,一切都应该是安全的。

读:

$fh = fopen($file, 'r');
if (flock($fh, LOCK_SH)) { // Play nice. Get a shared lock. This will block
                           // if another process has an exclusive lock.
    $string = fread($fh, filesize($file));
    flock($fh, LOCK_UN);
}
fclose($fh);

除了一些应该做的错误处理之外。

除了仍然存在竞争条件。

如果两个进程同时执行“读取 -> 递增 -> 写入”循环,则它们有可能都读取相同的数据,然后写入相同的数据。它没有计数器重置那么糟糕,但它是一个未命中的命中。为了解决这个问题,您需要将整个循环包装在一个独占锁中。所以基本上你最终会用这样的东西来使用它:

if(isset($_SESSION['visitWritten'])) {
    echo read($this->countFile);
} else {
    echo readAndIncrementAndUpdate($this->countFile);
}
于 2014-08-24T00:39:32.197 回答