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

PHP + AJAX 實現點擊量統計的安全高效方案

     編輯:PHP 綜合知識

原始程式碼安全漏洞剖析

原文範例存在嚴重 SQL 注入、無 CSRF 保護、同步阻塞請求、錯誤處理缺失、使用棄用 mysql_* 函式等生產環境不可接受的問題。

現代化重構:後端 API 篇

資料庫遷移(PDO + 預處理)

// migration
CREATE TABLE `page_views` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `page_key` VARCHAR(128) NOT NULL COMMENT '頁面唯一標識',
    `view_count` BIGINT UNSIGNED NOT NULL DEFAULT 0,
    `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_page_key` (`page_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

統計服務類

class ViewCounterService {
    private \PDO $pdo;
    public function __construct(\PDO $pdo) { $this->pdo = $pdo; }

    public function increment(string $pageKey): int {
        $sql = "INSERT INTO `page_views` (`page_key`, `view_count`) VALUES (?, 1)
                ON DUPLICATE KEY UPDATE `view_count` = `view_count` + 1";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$pageKey]);
        
        // 取得更新後的計數
        $stmt = $this->pdo->prepare("SELECT `view_count` FROM `page_views` WHERE `page_key` = ?");
        $stmt->execute([$pageKey]);
        return (int)$stmt->fetchColumn();
    }

    public function batchIncrement(array $pageKeys): array {
        // 事務批次處理,減少往返
        $this->pdo->beginTransaction();
        try {
            $stmt = $this->pdo->prepare("INSERT INTO `page_views` (`page_key`, `view_count`) VALUES (?, 1)
                ON DUPLICATE KEY UPDATE `view_count` = `view_count` + 1");
            foreach ($pageKeys as $key) { $stmt->execute([$key]); }
            $this->pdo->commit();
        } catch (\Throwable $e) {
            $this->pdo->rollBack();
            throw $e;
        }
        return $this->getCounts($pageKeys);
    }
}

API 控制器(含速率限制與 CSRF)

class ViewCounterController {
    private ViewCounterService $service;
    private RateLimiter $limiter;

    public function increment(Request $request): JsonResponse {
        // 1. CSRF 驗證(API 通常用 Token,此處示範同源策略 + SameSite Cookie)
        if (!$this->validateCsrf($request)) {
            return $this->jsonError('Invalid CSRF token', 403);
        }

        // 2. 參數驗證
        $pageKey = $request->input('page_key');
        if (empty($pageKey) || !preg_match('/^[a-zA-Z0-9_-]{1,128}$/', $pageKey)) {
            return $this->jsonError('Invalid page_key', 400);
        }

        // 3. 速率限制:每 IP 每分鐘最多 60 次
        $ip = $request->ip();
        if (!$this->limiter->attempt("view:increment:$ip", 60, 60)) {
            return $this->jsonError('Rate limit exceeded', 429);
        }

        // 4. 業務邏輯
        try {
            $count = $this->service->increment($pageKey);
            return response()->json(['count' => $count, 'page_key' => $pageKey]);
        } catch (\Throwable $e) {
            Log::error('View increment failed', ['error' => $e->getMessage(), 'page_key' => $pageKey]);
            return $this->jsonError('Internal server error', 500);
        }
    }
}

前端整合:非同步無感統計

原生 Fetch API 實現

async function trackView(pageKey) {
    const endpoint = '/api/view/increment';
    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
    
    try {
        const response = await fetch(endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'X-CSRF-TOKEN': csrfToken || '',
            },
            credentials: 'same-origin', // 攜帶 Cookie
            body: JSON.stringify({ page_key: pageKey }),
            // keepalive: true // 頁面卸載時仍發送
        });
        
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        updateViewDisplay(pageKey, data.count);
    } catch (error) {
        console.warn('View tracking failed:', error);
        // 靜默失效,不影響用戶體驗
    }
}

// 頁面可見時觸發(避免預渲染/後台標籤計數)
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
        const pageKey = document.body.dataset.pageKey;
        if (pageKey) trackView(pageKey);
    }
});

Vue 3 Composition API 封裝

// composables/useViewCounter.js
import { ref } from 'vue';
export function useViewCounter(pageKey) {
    const count = ref(0);
    const loading = ref(false);
    
    const fetch = async () => {
        loading.value = true;
        try {
            const res = await $api.post('/view/increment', { page_key: pageKey });
            count.value = res.data.count;
        } finally {
            loading.value = false;
        }
    };
    return { count, loading, fetch };
}

高併發優化方案

層級方案適用場景
應用層Redis INCR + 定時持久化到 MySQL高流量頁面(>1000 QPS)
應用層本地記憶體計數 + 批次寫入中等流量,單機部署
資料庫層MySQL 原子更新 + 覆蓋索引低中流量,強一致性要求
架構層寫入 Kafka/Redis Stream,消費者聚合入庫超大流量,最終一致性可接受

Redis 混合方案範例

class RedisViewCounter {
    private \Redis $redis;
    private \PDO $pdo;
    private string $luaScript = "
        local key = KEYS[1]
        local ttl = ARGV[1]
        local count = redis.call('INCR', key)
        if count == 1 then redis.call('EXPIRE', key, ttl) end
        return count
    ";
    
    public function increment(string $pageKey): int {
        $key = "views:{$pageKey}";
        $count = $this->redis->eval($this->luaScript, 1, $key, 86400); // 24h TTL
        
        // 異步持久化(佇列任務)
        if ($count % 100 === 0) { // 每 100 次觸發一次持久化
            dispatch(new SyncViewCountToDb($pageKey, $count));
        }
        return $count;
    }
}

總結

點擊量統計看似簡單,實則涉及資料一致性、高併發寫入、防刷控制、前端體驗、資料持久化策略等多維工程課題。摒棄同步阻塞、裸 SQL、無防護的原始寫法,採用「非同步前端 + 速率限制 API + Redis 熱點計數 + 異步落庫」的分層架構,才能支撐百萬級 PV 的穩定運行。

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