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

PHP 實現每日 IP 頁面訪問次數限制:數據庫計數器與定時清理

     編輯:PHP 相關知識

表結構

CREATE TABLE `ip_daily_limit` (
    `ip` VARBINARY(16) NOT NULL COMMENT 'IPv4: 4字節, IPv6: 16字節',
    `access_date` DATE NOT NULL COMMENT '訪問日期',
    `page_url` VARCHAR(255) NOT NULL COMMENT '頁面 URL 或標識',
    `visit_count` SMALLINT UNSIGNED NOT NULL DEFAULT 1,
    PRIMARY KEY (`ip`, `access_date`, `page_url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IP 每日頁面訪問限制';
  • 聯合主鍵 (ip, access_date, page_url) 保證同一天同頁面同 IP 唯一
  • VARBINARY(16) 兼容 IPv4/IPv6,PHP 存儲用 inet_pton()

核心邏輯 (單頁面限制)

<?php
function checkDailyLimit(string $pageId, int $maxVisits = 100): bool {
    $mysqli = new mysqli('localhost', 'user', 'pass', 'db');
    $mysqli->set_charset('utf8mb4');

    $ipBin = inet_pton($_SERVER['REMOTE_ADDR']); // 二進制 IP
    $today = date('Y-m-d');

    // 原子化:查找或創建 + 計數器遞增
    $sql = "
        INSERT INTO ip_daily_limit (ip, access_date, page_url, visit_count)
        VALUES (?, ?, ?, 1)
        ON DUPLICATE KEY UPDATE
            visit_count = visit_count + 1
    ";
    $stmt = $mysqli->prepare($sql);
    $stmt->bind_param('bss', $ipBin, $today, $pageId);
    $stmt->execute();

    // 獲取更新後的計數
    $sql2 = "SELECT visit_count FROM ip_daily_limit WHERE ip=? AND access_date=? AND page_url=?";
    $stmt2 = $mysqli->prepare($sql2);
    $stmt2->bind_param('bss', $ipBin, $today, $pageId);
    $stmt2->execute();
    $result = $stmt2->get_result();
    $row = $result->fetch_assoc();

    return ($row['visit_count'] <= $maxVisits);
}

// 使用
$pageId = 'article_detail'; // 或具體文章 ID
if (!checkDailyLimit($pageId, 50)) {
    http_response_code(429);
    die('今日訪問次數已達上限 (50次),請明天再來。');
}
// 正常輸出頁面
?>

全站總訪問限制 (不區分頁面)

CREATE TABLE `ip_daily_total` (
    `ip` VARBINARY(16) NOT NULL,
    `access_date` DATE NOT NULL,
    `total_visits` INT UNSIGNED NOT NULL DEFAULT 1,
    PRIMARY KEY (`ip`, `access_date`)
);

-- 邏輯同上,只需去掉 page_url 字段
?>

定時清理過期數據 (Cron)

<?php
// cleanup_ip_log.php - 每天凌晨 00:05 執行
require 'db_config.php';

$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
$keepDays = 30; // 保留 30 天
$cutoff = date('Y-m-d', strtotime("-$keepDays days"));

$sql = "DELETE FROM ip_daily_limit WHERE access_date < ?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('s', $cutoff);
$stmt->execute();

echo "清理完成,影響行數: " . $mysqli->affected_rows . "\n";
?>

Crontab:5 0 * * * /usr/bin/php /path/cleanup_ip_log.php >> /var/log/ip_cleanup.log 2>&1

性能優化方案

方案優勢劣勢適用場景
MySQL 計數器 (本文)持久化、可審計、無額外組件寫入壓力大、需索引優化中小流量、已有 MySQL、需合規留存
Redis INCR + EXPIRE極高性能、原子操作、自動過期需額外組件、數據易丟失高併發、限流、驗證碼、API 閘道
Memcached + CAS分布式、內存級速度無持久化、CAS 重試複雜集群環境、非關鍵計數
Nginx limit_req_zone應用層無感、擋住大流量邏輯不靈活、無法按業務 ID 限制第一道防線、靜態資源、登錄接口

Redis 實現對比 (推薦高併發)

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$key = "daily_limit:{$_SERVER['REMOTE_ADDR']}:{$pageId}:".date('Ymd');
$count = $redis->incr($key);
if ($count === 1) {
    $redis->expireAt($key, strtotime('tomorrow')); // 午夜過期
}
if ($count > 50) {
    http_response_code(429);
    die('今日訪問超限');
}
?>

避坑指南

  1. 代理 IP 穿透:確保 $_SERVER['REMOTE_ADDR'] 為真實客戶端 IP (配置 Nginx real_ip_header + set_real_ip_from)
  2. 時區一致性:PHP date('Y-m-d')、MySQL CURDATE()、系統時區必須統一 (建議全鏈路 UTC)
  3. 主鍵衝突死鎖:高併發下 ON DUPLICATE KEY UPDATE 可能產生間隙鎖死鎖,必要時捕獲 1213 錯誤碼重試
  4. 分區表優化:大表按 access_date 分區,DROP PARTITION 秒級清理歷史數據
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved