首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 文件計數器實現訪問量統計:併發安全與性能優化

     編輯:PHP 綜合知識

場景與挑戰

文件計數器是無數據庫環境下統計頁面訪問量(PV)的經典方案。核心挑戰:高併發下的競態條件(丟失更新)、I/O 性能瓶頸、文件鎖可靠性。

方案演進

版本 1:朴素實現(有 Bug)

<?php
// ❌ 非原子操作,併發會丟失計數
$file = 'counter.txt';
$count = intval(file_get_contents($file)) + 1;
file_put_contents($file, $count);
echo $count;
?>

版本 2:flock 文件鎖(標準方案)

<?php
function fileCounter(string $file = 'counter.txt'): int {
    // 以讀寫模式打開,創建文件
    $fp = fopen($file, 'c+');
    if (!$fp) {
        throw new \RuntimeException("無法打開計數文件: $file");
    }
    
    // 獨佔鎖(阻塞等待)
    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        throw new \RuntimeException("獲取文件鎖失敗");
    }
    
    try {
        // 讀取當前值
        $count = (int) fread($fp, filesize($file) ?: 1);
        $count++;
        
        // 截斷並寫入
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, (string)$count);
        fflush($fp); // 確保寫入磁盤
        
        // 釋放鎖
        flock($fp, LOCK_UN);
    } finally {
        fclose($fp);
    }
    
    return $count;
}

// 使用
echo "訪問量: " . fileCounter() . " 次";
?>

版本 3:高性能優化(減少系統調用)

<?php
class HighPerformanceCounter {
    private string $file;
    private int $bufferSize = 100; // 緩衝區:每 100 次寫一次盤
    private int $memCount = 0;
    
    public function __construct(string $file = 'counter.txt') {
        $this->file = $file;
        // 啟動時讀取基準值
        $this->memCount = (int) @file_get_contents($file);
    }
    
    public function increment(): int {
        $this->memCount++;
        
        // 達到緩衝閾值或進程結束時持久化
        if ($this->memCount % $this->bufferSize === 0) {
            $this->flush();
        }
        
        return $this->memCount;
    }
    
    public function flush(): bool {
        // 使用臨時文件 + rename 實現原子寫入(無需鎖)
        $tmpFile = $this->file . '.tmp.' . random_bytes(4);
        $result = file_put_contents($tmpFile, (string)$this->memCount, LOCK_EX);
        if ($result === false) return false;
        
        // rename 在同一文件系統上是原子操作
        return @rename($tmpFile, $this->file);
    }
    
    public function __destruct() {
        $this->flush();
    }
}

// 註冊關閉函數確保持久化
$counter = new HighPerformanceCounter();
register_shutdown_function([$counter, 'flush']);

echo "PV: " . $counter->increment();
?>

版本 4:分佈式/多進程環境(Redis/數據庫)

<?php
// 多台服務器/多進程必須用共享存儲
class DistributedCounter {
    private \Redis $redis;
    private string $key;
    
    public function __construct(\Redis $redis, string $key = 'page:pv:home') {
        $this->redis = $redis;
        $this->key = $key;
    }
    
    public function increment(): int {
        // 原子自增
        return $this->redis->incr($this->key);
    }
    
    public function get(): int {
        return (int) $this->redis->get($this->key);
    }
    
    // 定期同步到文件/數據庫持久化
    public function syncToFile(string $file): bool {
        $count = $this->get();
        return file_put_contents($file, (string)$count, LOCK_EX) !== false;
    }
}
?>

性能對比測試

<?php
function benchmark(callable $fn, int $iterations = 10000): float {
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return microtime(true) - $start;
}

// 測試環境:SSD、PHP 8.2、Ubuntu 22.04
echo "flock 版本: " . benchmark('fileCounter') . " 秒\n";           // ~2.5秒 (10k次)
echo "緩衝版本: " . benchmark(fn() => $counter->increment()) . " 秒\n"; // ~0.08秒
echo "Redis 版本: " . benchmark(fn() => $redisCounter->increment()) . " 秒\n"; // ~0.3秒
?>

Nginx 訪問日誌替代方案(零代碼侵入)

# Nginx 配置
log_format pv_log '$remote_addr [$time_local] "$request" $status $request_time';
access_log /var/log/nginx/pv.log pv_log;

# 統計腳本(定時任務運行)
# bash: awk '{print $1}' /var/log/nginx/pv.log | sort | uniq -c | sort -rn
# 或使用 GoAccess、ELK、ClickHouse 做實時分析

常見問題排查

問題原因解決
計數器歸零文件被誤刪/權限錯誤/磁盤滿備份機制、監控告警、磁盤配額
計數異常增長爬蟲/刷流量/重複請求User-Agent 過濾、IP 限流、JS 渲染後統計
併發下數字不準無鎖/鎖失效/緩衝區未刷新flock/Redis 原子操作、register_shutdown_function
性能差頻繁 fsync/文件系統鎖爭用批量寫入、內存緩衝、異步持久化
NFS 掛載失效flock 在 NFS 上不可靠遷移到 Redis/數據庫、或使用本地磁盤 + 定時同步

生產級完整實現

<?php
namespace App\Metrics;

class PageViewCounter {
    private string $file;
    private int $bufferSize;
    private int $count = 0;
    private bool $dirty = false;
    
    public function __construct(
        string $file = '/var/www/data/pv_counter.txt',
        int $bufferSize = 100
    ) {
        $this->file = $file;
        $this->bufferSize = $bufferSize;
        $this->count = (int) @file_get_contents($file);
        
        // 確保目錄存在
        $dir = dirname($file);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
    }
    
    public function hit(): int {
        $this->count++;
        $this->dirty = true;
        
        if ($this->count % $this->bufferSize === 0) {
            $this->persist();
        }
        
        return $this->count;
    }
    
    public function get(): int {
        return $this->count;
    }
    
    public function persist(): bool {
        if (!$this->dirty) return true;
        
        $tmpFile = $this->file . '.tmp.' . spl_object_id($this);
        $bytes = @file_put_contents($tmpFile, (string)$this->count, LOCK_EX);
        if ($bytes === false) {
            error_log("PV 計數器寫入失敗: {$this->file}");
            return false;
        }
        
        if (!@rename($tmpFile, $this->file)) {
            @unlink($tmpFile);
            error_log("PV 計數器重命名失敗: {$this->file}");
            return false;
        }
        
        $this->dirty = false;
        return true;
    }
    
    public function __destruct() {
        $this->persist();
    }
}

// 使用示例(單例模式)
class PV {
    private static ?PageViewCounter $instance = null;
    
    public static function init(string $file = '/var/www/data/pv.txt'): void {
        self::$instance = new PageViewCounter($file);
    }
    
    public static function hit(): int {
        return self::$instance?->hit() ?? 0;
    }
    
    public static function get(): int {
        return self::$instance?->get() ?? 0;
    }
}

// bootstrap.php
PV::init();

// controller
PV::hit();
// 模板顯示: 總訪問  次
?>

總結

文件計數器適用於單機、低併發、非關鍵業務場景。核心原則:flock 保證原子性、緩衝區減少 I/O、臨時文件 + rename 保證寫入原子性、關閉函數保證持久化。多機佈署、高併發、數據強一致性要求場景,必須遷移到 Redis/數據庫原子計數器。現代架構建議:前端埋點 + 日誌分析/時序數據庫 替代服務端計數器,實現全鏈路可觀測。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved