PHP IP黑名单怎么存

wen PHP项目 2

PHP IP黑名单存储方案深度解析:从文件到Redis的架构演进与性能权衡


目录导读

  1. 为什么IP黑名单的存储方式至关重要?
  2. 纯文本/JSON文件存储——简单但脆弱的起点
  3. MySQL数据库存储——事务性与查询灵活性的平衡
  4. Redis有序集合(Sorted Set)——高性能封禁的工业标准
  5. 内存数组/APCu——极致速度下的场景局限
  6. 混合架构:多层缓存+Lazy淘汰策略实战
  7. 常见问题FAQ(高频面试与实战痛点)
  8. 选择存储方案的核心决策树

为什么IP黑名单的存储方式至关重要?

在构建Web应用防火墙、防暴力破解或地域封禁功能时,IP黑名单的存储引擎直接决定了三个关键指标:查询延迟(每次请求的额外开销)、写入/更新效率(封禁操作的即时性)、以及可扩展性(当黑名单条目从千级增长到百万级时),错误的存储选型可能导致在高并发下数据库连接池被击穿,或导致每次请求都进行磁盘I/O从而拖垮PHP-FPM进程,本文基于业内主流实践,结合WordPress防暴力插件、Laravel防刷中间件及高性能API网关的源码思路,为你揭示如何选择“够用且不浪费”的存储方案。

PHP IP黑名单怎么存


方案一:纯文本/JSON文件存储——简单但脆弱的起点

实现原理:将IP列表存储于blacklist.json.txt中,通过file_get_contents + in_array(或stripos)匹配。
适用场景:单机开发环境、IP数量<1000且并发极低的内部工具。
致命缺陷

  • 无原子性:多进程写入时文件锁(flock)容易造成阻塞或数据丢失。
  • 无索引:百万级IP时,每次in_array都是O(n)的CPU灾难。
  • 无TTL:无法实现临时封禁(如10分钟自动解封)。

改进伪代码

// 读取一次,缓存于全局变量
$blacklist = json_decode(@file_get_contents('blacklist.json'), true);
// 需配合opcache缓存到共享内存,否则每个请求都读磁盘

方案二:MySQL数据库存储——事务性与查询灵活性的平衡

建表规范

CREATE TABLE ip_blacklist (
  id INT AUTO_INCREMENT PRIMARY KEY,
  ip VARCHAR(45) NOT NULL,  -- 兼容IPv6
  reason VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  expires_at DATETIME NULL,  -- NULL为永久封禁
  UNIQUE KEY idx_ip (ip)
) ENGINE=InnoDB;

查询优化:必须配合覆盖索引(只查ip字段)并使用SELECT 1 FROM ip_blacklist WHERE ip = ? AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1
隐患

  • 每次HTTP请求都打一次数据库,即使有连接池,也会增加2-5ms延迟。
  • 使用IN查询批量放行白名单时,索引失效风险高。
  • 适用于管理后台操作(封禁/解封)而非高频只读请求链。

方案三:Redis有序集合(Sorted Set)——高性能封禁的工业标准

为什么选择Sorted Set而非Set?

  • Set:支持O(1)判断成员,但无法自动过期。
  • Sorted Set:利用score存入过期时间戳,通过ZREMRANGEBYSCORE清理过期,且ZSCORE命令可在毫秒级判断IP是否存在及剩余封禁时长。

核心操作代码片段

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'blacklist:ip';
// 封禁IP:score = 过期时间戳(永久则 -1)
$expire = strtotime('+1 hour');
$redis->zAdd($key, $expire, $clientIp);
// 校验IP是否被封
$banUntil = $redis->zScore($key, $clientIp);
if ($banUntil !== false && ($banUntil === -1 || $banUntil > time())) {
    http_response_code(403);
    exit('Forbidden');
}
// 后台定时清理任务
$redis->zRemRangeByScore($key, 0, time());

性能优势:单机Redis可达10万+ QPS,内存有效压缩(用ip2long()转成INT存储可减少50%内存)。
注意:避免使用KEYS *命令,应使用ZRANGEBYSCORE分页处理。


方案四:内存数组/APCu——极致速度下的场景局限

APCu(APC User Cache) 适合单台PHP-FPM服务器:

if (apcu_exists('blacklist:'.ip2long($ip))) { // 拒绝 }

局限性

  • 不支持分布式,无法做多节点同步。
  • 重启PHP-FPM后缓存丢失。
  • 仅能存储字符串,需要自行序列化结构化数据。

适合作为第一层过滤器(快速拒绝已知恶意IP),配合数据库或Redis作为持久层。


混合架构:多层缓存+Lazy淘汰策略实战

推荐生产级方案(解决“临时封禁+永久封禁+批量导入”矛盾)

  1. 第一层:PHP本地内存(静态变量)存储最近10分钟访问过的IP封禁状态,命中则直接拦截(节省Redis网络开销)。
  2. 第二层:Redis Sorted Set负责全量校验和过期管理。
  3. 第三层:MySQL存储封禁原因、操作员审计日志,并定期同步到Redis(比如每分钟订阅Binlog变更)。

伪代码逻辑

function isBlocked($ip) {
    $staticCache = &$_ENV['ip_block_cache'];
    $key = ip2long($ip);
    if (isset($staticCache[$key]) && $staticCache[$key] > time()) return true;
    $banUntil = $redis->zScore($key);
    if ($banUntil === false) {
        $staticCache[$key] = 0; // 标记白名单缓存60秒
    } else {
        $staticCache[$key] = $banUntil > time() ? $banUntil : 0;
    }
    return $staticCache[$key] > time();
}

常见问题FAQ(高频面试与实战痛点)

Q1:黑名单IP数量达到千万级,Redis内存会不会爆?
A:IPv4使用ip2long转INT后存储,每个条目的key固定4字节+Score 8字节,1000万条大约占用120MB内存,但建议只存储活跃封禁IP,过期数据定时清理。

Q2:如何实现“通配符封禁”(如封禁192.168.)?
A:不要用子网掩码,改用CIDR计算,在Redis中可用SCAN匹配,但性能差;更优解是存储网段前缀(例如168.0.0/16),在PHP中用ip2long转二进制并做位运算判断。

Q3:为什么不直接存Set
A:Set无过期时间,一旦封禁必须手动清理,容易产生僵尸数据;且无法实现“封禁到具体时间点”的功能。

Q4:数据库与Redis数据不一致怎么办?
A:以Redis为主数据源,MySQL仅作审计,通过监听DEL操作触发数据库解封,或每5分钟全量对比一次。


选择存储方案的核心决策树

  • 并发 < 100且单机 → 直接使用APCu或文件缓存,加file_put_contents锁定。
  • 有集群需求且IP量 < 1万 → MySQL唯一索引足够,查询延迟可接受。
  • 高并发(>1000 QPS)且需要临时封禁 → 必须上Redis,并采用混合三层架构。
  • 需要模糊匹配(MAC直接封网段) → 建议引入Nginx+Lua(OpenResty)配合ipset,让PHP不必承担流量层拦截压力。

没有最好的方案,只有最匹配业务场景的方案,建议先压测你的URL响应时间阈值(例如P99 < 200ms),然后选择最轻量、运维成本最低的存储引擎——因为黑名单本身就是防御措施,它的性能开销绝不能成为攻击者的另一突破口。

抱歉,评论功能暂时关闭!