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

PHP 利用 Memcached 實現 IP 訪問頻率限制 (滑動窗口算法)

     編輯:PHP 相關知識

核心邏輯

使用 Memcached 存儲每個 IP 的請求計數器與時間窗口,採用**滑動窗口**策略:

  • 鍵:ratelimit:{IP}
  • 值:[count, window_start, last_request_time, status]
  • 超過限制時設置 status = 'BLOCKED' 並記錄封禁開始時間

完整實現代碼

<?php
/**
 * Memcached 基礎頻率限制器
 * 適用於單機或小規模集群,高併發建議使用 Redis + Lua 腳本保證原子性
 */

$config = [
    'memcached_host' => '127.0.0.1',
    'memcached_port' => 11211,
    'max_requests'   => 50,        // 時間窗口內最大請求數
    'window_seconds' => 60,        // 時間窗口 (秒)
    'block_seconds'  => 300,       // 封禁時長 (秒)
];

$mem = new Memcached();
$mem->addServer($config['memcached_host'], $config['memcached_port']);

$ip = $_SERVER['REMOTE_ADDR'];
$key = "ratelimit:$ip";

$data = $mem->get($key);
$now = time();

if ($data === false) {
    // 首次訪問
    $data = [
        'count' => 1,
        'window_start' => $now,
        'last_request' => $now,
        'status' => 'OK'
    ];
} else {
    // 解包數據
    [$count, $windowStart, $lastRequest, $status] = $data;

    // 1. 處理封禁狀態
    if ($status === 'BLOCKED') {
        $blockedElapsed = $now - $windowStart; // 這裡 window_start 存的是封禁開始時間
        if ($blockedElapsed < $config['block_seconds']) {
            $wait = $config['block_seconds'] - $blockedElapsed;
            http_response_code(429);
            die("請求過於頻繁,已暫時封禁。請等待 $wait 秒後重試。");
        } else {
            // 封禁期結束,重置
            $count = 0;
            $windowStart = $now;
            $status = 'OK';
        }
    }

    // 2. 滑動窗口檢查
    $elapsed = $now - $windowStart;
    if ($elapsed >= $config['window_seconds']) {
        // 窗口已過期,重置計數器
        $count = 1;
        $windowStart = $now;
    } else {
        // 窗口內,增加計數
        $count++;
        if ($count > $config['max_requests']) {
            // 觸發封禁
            $status = 'BLOCKED';
            $windowStart = $now; // 重用字段存封禁開始時間
            $mem->set($key, [$count, $windowStart, $now, $status], $config['block_seconds'] + 10);
            http_response_code(429);
            die("請求超過限制 (${config['max_requests']} 次/${config['window_seconds']} 秒),IP 已封禁 ${config['block_seconds']} 秒。");
        }
    }

    $data = [$count, $windowStart, $now, $status];
}

// 3. 寫回 Memcached (TTL 略大於窗口/封禁時間)
$ttl = max($config['window_seconds'], $config['block_seconds']) + 10;
$mem->set($key, $data, $ttl);

// 正常業務邏輯繼續...
echo "請求通過。當前窗口請求數: $count";
?>

關鍵點解析

環節說明
鍵設計ratelimit:{IP} 簡單直觀,集群環境自動分片
數據結構數組 [count, window_start, last_request, status] 緊湊高效
滑動窗口每次請求檢查 now - window_start,過期則重置,避免固定窗口邊界突發
封禁機制狀態機:OK → BLOCKED → OK,封禁期內直接拒絕,不消耗應用資源
TTL 設置略大於最大窗口/封禁時間,自動清理過期鍵,防止內存洩漏
并發安全Memcached get/set 非原子,**高併發下可能漏計/重複計數**。嚴格場景請用 Redis + Lua 或 Memcached CAS

進階:使用 Memcached CAS (Check-And-Set) 保證原子性

<?php
$mem = new Memcached();
$mem->setOption(Memcached::OPT_COMPRESSION, false);

$key = "ratelimit:$ip";
$maxAttempts = 3;

for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
    $data = $mem->get($key, null, $casToken);
    if ($data === false) {
        $data = ['count'=>1, 'window'=>$now, 'status'=>'OK'];
        if ($mem->set($key, $data, $ttl)) break;
        continue;
    }

    [$count, $windowStart, $status] = $data;
    // ... 邏輯判斷更新 $count, $windowStart, $status ...

    $data = [$count, $windowStart, $status];
    if ($mem->cas($casToken, $key, $data, $ttl)) {
        break; // 成功
    }
    // CAS 失敗,說明數據被其他進程修改,重試
    usleep(10000); // 10ms
}
?>

生產環境建議:遷移至 Redis + Lua

Redis 單線程模型 + Lua 腳本原子執行,完美解決併發競爭,性能更強:

-- Redis Lua 腳本 (ATOMIC)
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local block = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

local data = redis.call('HMGET', key, 'count', 'window_start', 'status', 'block_start')
local count = tonumber(data[1]) or 0
local windowStart = tonumber(data[2]) or now
local status = data[3] or 'OK'
local blockStart = tonumber(data[4]) or 0

if status == 'BLOCKED' then
    if now - blockStart < block then
        return {0, block - (now - blockStart)} -- 拒絕,返回剩餘封禁秒數
    else
        status = 'OK'
        count = 0
        windowStart = now
    end
end

if now - windowStart >= window then
    count = 1
    windowStart = now
else
    count = count + 1
    if count > limit then
        redis.call('HMSET', key, 'count', count, 'window_start', windowStart, 'status', 'BLOCKED', 'block_start', now)
        redis.call('EXPIRE', key, block + 10)
        return {0, block} -- 拒絕,返回封禁時長
    end
end

redis.call('HMSET', key, 'count', count, 'window_start', windowStart, 'status', status)
redis.call('EXPIRE', key, window + 10)
return {1, count} -- 允許

PHP 調用:$redis->eval($luaScript, 1, $key, $limit, $window, $block, time())

總結

  1. Memcached 版適合**中小流量、單機/簡單集群**、代碼簡單易維護
  2. 高併發、嚴格限流、分布式環境**強烈建議 Redis + Lua**
  3. 結合 Nginx limit_req 形成**雙層防護**:Nginx 擋住大流量洪峰,應用層精細控制業務邏輯
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved