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

PHP + Redis 實現廣告點擊統計緩存,大幅降低 MySQL 壓力

     編輯:PHP 綜合知識

業務場景與痛點

廣告系統需統計每個廣告的點擊數(PV/UV)。高併發下直接寫 MySQL 會導致:

  • 行鎖競爭,更新效能急劇下降。
  • 主從同步延遲,統計數據不準。
  • 連接數耗盡,影響核心業務。

解決方案:Redis 計數器聚合 + 定時/閾值觸發持久化到 MySQL。

核心架構設計

pconnect('127.0.0.1', 6379);

$mysql = new PDO('mysql:host=127.0.0.1;dbname=ad_db', 'user', 'pass');
$mysql->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// 鍵名規範
const KEY_AD_MAP = 'ad:url_map';          // url_md5 -> ad_id (Hash)
const KEY_AD_EXISTS = 'ad:init_done';     // 初始化標記 (String)
const KEY_AD_CLICK_PREFIX = 'ad:click:';  // ad_id -> 計數器 (String)
const FLUSH_THRESHOLD = 100;              // 閾值觸發落庫
const FLUSH_INTERVAL = 300;               // 5 分鐘定時任務兜底

關鍵流程實現

1. 啟動時初始化廣告映射(僅執行一次)

function initAdMapping(PDO $mysql, Redis $redis): void {
    if ($redis->exists(KEY_AD_EXISTS)) return;

    $stmt = $mysql->query('SELECT id, adve_url FROM user_online_adve');
    $pipe = $redis->multi();
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $urlHash = md5($row['adve_url']);
        $pipe->hSet(KEY_AD_MAP, $urlHash, $row['id']);
    }
    $pipe->set(KEY_AD_EXISTS, 1);
    $pipe->exec();
}

2. 點擊請求處理(高併發熱點路徑)

function handleClick(Redis $redis, string $referer): void {
    $urlHash = md5($referer);
    $adId = $redis->hGet(KEY_AD_MAP, $urlHash);
    if (!$adId) return; // 非廣告鏈接,直接返回

    $clickKey = KEY_AD_CLICK_PREFIX . $adId;
    $count = $redis->incr($clickKey); // 原子自增,極高效能

    // 閾值觸發異步落庫(生產建議推入隊列由 Worker 處理)
    if ($count >= FLUSH_THRESHOLD) {
        // Lua 腳本保證「讀取並歸零」原子性
        $lua = <<= tonumber(ARGV[1]) then
            redis.call('SET', KEYS[1], 0)
            return v
        end
        return 0
        LUA;
        $flushed = $redis->eval($lua, 1, $clickKey, FLUSH_THRESHOLD);
        if ($flushed) {
            // 實際生產:推送到 Redis List / Kafka / Swoole Task
            // 這裡演示同步寫入
            flushToMysql($adId, (int)$flushed);
        }
    }
}

3. 定時任務兜底落庫(補償機制)

function cronFlush(PDO $mysql, Redis $redis): void {
    $keys = $redis->keys(KEY_AD_CLICK_PREFIX . '*');
    if (!$keys) return;

    $pipe = $redis->multi();
    foreach ($keys as $key) {
        $pipe->get($key);
        $pipe->set($key, 0); // 歸零
    }
    $counts = $pipe->exec();

    $sql = 'UPDATE user_online_adve SET adve_num = adve_num + :cnt WHERE id = :id';
    $stmt = $mysql->prepare($sql);
    foreach ($keys as $i => $key) {
        $cnt = (int)$counts[$i * 2]; // GET 結果
        if ($cnt > 0) {
            $adId = (int)str_replace(KEY_AD_CLICK_PREFIX, '', $key);
            $stmt->execute([':cnt' => $cnt, ':id' => $adId]);
        }
    }
}

進階優化:Lua 腳本保證原子性

上述閾值觸發存在競態:incr 後讀取、判斷、歸零非原子。生產環境**必須**用 Lua:

$luaScript = <<<'LUA'
local clickKey = KEYS[1]
local threshold = tonumber(ARGV[1])
local adId = ARGV[2]
local flushKey = 'ad:flush_queue'

local cnt = redis.call('INCR', clickKey)
if cnt >= threshold then
    redis.call('SET', clickKey, 0)
    redis.call('LPUSH', flushKey, adId .. ':' .. cnt)
end
return cnt
LUA;

$redis->eval($luaScript, 1, $clickKey, FLUSH_THRESHOLD, $adId);

Worker 進程消費 ad:flush_queue 列表批次寫入 MySQL,實現真正的「寫入不阻塞、數據不丟失」。

擴展:UV 去重統計

若需統計唯一訪客(UV),使用 Redis HyperLogLog 或 Set:

function handleUniqueClick(Redis $redis, string $referer, string $userId): void {
    $urlHash = md5($referer);
    $adId = $redis->hGet(KEY_AD_MAP, $urlHash);
    if (!$adId) return;

    $uvKey = "ad:uv:{$adId}";
    // PFADD 返回 1 表示新增元素(首次訪問)
    if ($redis->pfAdd($uvKey, $userId)) {
        $redis->incr("ad:uv_count:{$adId}");
    }
    // 同樣處理 PV 計數
    handleClick($redis, $referer);
}

監控與告警

  • 監控 Redis 記憶體佔用、Key 過期情況。
  • 監控落庫延遲(Redis 計數器值 - MySQL 值),超過閾值告警。
  • 定期核對 Redis 與 MySQL 總數一致性。

總結

Redis 作為「寫緩衝層」吸收高併發寫入,通過「閾值觸發 + 定時兜底 + Lua 原子操作」實現數據最終一致性落庫,可將 MySQL 寫入壓力降低 **99% 以上**,是廣告、計數、日誌等高寫場景的標準架構模式。

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