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 字段
?>
<?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 限制 | 第一道防線、靜態資源、登錄接口 |
<?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('今日訪問超限');
}
?>
$_SERVER['REMOTE_ADDR'] 為真實客戶端 IP (配置 Nginx real_ip_header + set_real_ip_from)date('Y-m-d')、MySQL CURDATE()、系統時區必須統一 (建議全鏈路 UTC)ON DUPLICATE KEY UPDATE 可能產生間隙鎖死鎖,必要時捕獲 1213 錯誤碼重試access_date 分區,DROP PARTITION 秒級清理歷史數據