CREATE TABLE `ip_access_log` (
`ip` INT UNSIGNED NOT NULL COMMENT 'IP 數值 (INET_ATON)',
`last_access` INT UNSIGNED NOT NULL COMMENT '上次訪問時間戳',
`access_count` SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '窗口內訪問次數',
`window_start` INT UNSIGNED NOT NULL COMMENT '窗口起始時間戳',
PRIMARY KEY (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IP 訪問頻率限制表';
ip 使用 INT UNSIGNED 存儲 INET_ATON() 轉換的數值,支持索引、範圍查詢、比較運算<?php
$ip = $_SERVER['REMOTE_ADDR'];
$now = time();
$window = 3600; // 1 小時窗口
$limit = 100; // 最大訪問次數
$sql = "
INSERT INTO ip_access_log (ip, last_access, access_count, window_start)
VALUES (INET_ATON(?), ?, 1, ?)
ON DUPLICATE KEY UPDATE
last_access = VALUES(last_access),
access_count = CASE
WHEN VALUES(last_access) - window_start >= ? THEN 1
ELSE access_count + 1
END,
window_start = CASE
WHEN VALUES(last_access) - window_start >= ? THEN VALUES(window_start)
ELSE window_start
END
";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('siii', $ip, $now, $now, $window, $window);
$stmt->execute();
?>
<?php
$sql = "
SELECT access_count, window_start
FROM ip_access_log
WHERE ip = INET_ATON(?)
AND (window_start + ?) > ?
";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('sii', $ip, $window, $now);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row && $row['access_count'] > $limit) {
http_response_code(429);
die("訪問過於頻繁,請稍後再試。");
}
// 允許訪問,繼續業務邏輯
?>
<?php
// 每天凌晨執行
$expireTime = time() - 86400 * 7; // 保留 7 天
$mysqli->query("DELETE FROM ip_access_log WHERE last_access < $expireTime");
?>
<?php
// PHP 內建函數 (推薦,不依賴 MySQL)
$ip = '192.168.1.101';
$long = ip2long($ip); // 有符號整數,可能為負
$unsigned = sprintf('%u', $long); // 轉無符號字符串
echo "數值: $unsigned\n"; // 3232235877
// 還原
$ipRestored = long2ip($long);
echo "還原: $ipRestored\n"; // 192.168.1.101
// MySQL 函數 (SQL 中使用)
/*
SELECT INET_ATON('192.168.1.101'); -- 3232235877
SELECT INET_NTOA(3232235877); -- 192.168.1.101
*/
?>
若需**精確滑動窗口** (而非固定窗口),需記錄每次請求時間戳:
CREATE TABLE `ip_request_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`ip` INT UNSIGNED NOT NULL,
`request_time` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_ip_time` (`ip`, `request_time`)
) ENGINE=InnoDB;
-- 記錄請求
INSERT INTO ip_request_log (ip, request_time) VALUES (INET_ATON(?), ?);
-- 統計窗口內請求數
SELECT COUNT(*) FROM ip_request_log
WHERE ip = INET_ATON(?)
AND request_time > ?; -- now - window
-- 定期清理
DELETE FROM ip_request_log WHERE request_time < ?; -- now - max_window
權衡:精確度高但寫入量大、表膨脹快,需分區/定期清理。適合核心接口 (登錄、支付、API 閘道)。
ENGINE=MEMORY,重啟丟失但速度最快INCR + EXPIRE 性能優於 MySQL 10-100 倍MySQL 方案適合**中小流量、無 Redis 環境、需持久化審計**的場景。核心技巧:
INET_ATON 將 IP 轉無符號整數,利用數值索引高效查詢INSERT ... ON DUPLICATE KEY UPDATE 原子化「查找或創建/更新」CASE WHEN 完成,減少網絡往返