不能只存IP字符串,因为缺乏时间窗口标识会导致计数混叠且无法自动过期;必须将时间窗口(如date('YmdHi'))嵌入key,并设EX 60 TTL,用Lua原子执行INCR+EXPIRE,同时正确获取真实IP并做格式校验。

PHP里用Redis做IP限流,为什么不能只存IP字符串
因为单纯存 192.168.1.100 会导致不同时间窗口的计数混在一起,没法自动过期。必须把时间窗口信息揉进key里,比如按分钟切片就用 "rate:ip:192.168.1.100:202405201435" 这种格式。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
date('YmdHi')生成分钟级窗口标识,避免秒级key爆炸(每秒一个key撑不住) - 不要用
time()做key后缀——它导致每秒都新建key,TTL设再短也压不住key数量增长 - Redis key长度别超80字符,长IP+长日期拼接容易超,可对IP做简单哈希(如
sprintf('%x', ip2long($ip))) - 务必给每个key设TTL,例如
EX 60,否则窗口切换后旧key残留,计数失真
PHP判断是否超限的三步原子操作怎么写才安全
靠 GET + INCR + EXPIRE 分三步会出竞态:两个请求几乎同时进来,都读到0,都+1,结果本该限2次却放行3次。必须用Lua脚本保证原子性。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
eval执行内联Lua,不要分多次调Redis命令 - 脚本里先
incr,再ttl判断key是否存在,不存在就expire - 返回值统一为当前计数值,PHP层再比对阈值,别在脚本里做if判断后返回布尔——不方便调试
- 示例关键片段:
return redis.call('INCR', KEYS[1]),配合redis->eval($script, 1, $key)
$_SERVER['REMOTE_ADDR']拿不到真实IP时怎么办
当API前有Nginx、CDN或云WAF时,$_SERVER['REMOTE_ADDR'] 返回的是代理IP,不是用户真实IP,直接用来限流会误伤或失效。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 优先检查
$_SERVER['HTTP_X_FORWARDED_FOR'],取最左边第一个非私有地址(过滤掉127.0.0.1、10.0.0.0/8等) - Cloudflare用户必须读
$_SERVER['HTTP_CF_CONNECTING_IP'],它比X-Forwarded-For更可靠 - 永远校验IP格式:
filter_var($ip, FILTER_VALIDATE_IP),防止伪造头注入恶意字符串 - 别信
$_SERVER['HTTP_X_REAL_IP']—— Nginx没配set_real_ip_from时它和REMOTE_ADDR一样不可靠
并发高时Redis连接打满或超时怎么缓解
每请求都连Redis、执行Lua、断开,QPS上200就容易触发连接池耗尽或 Connection timed out 错误。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
Predis\Client或Redis扩展的持久连接(['scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'timeout' => 0.1]) - 把限流逻辑放到前置中间件(如Swoole HTTP Server的onRequest),而不是每个业务接口里重复写
- 阈值别设太低(如每分钟5次),高频调用场景建议改用滑动窗口(需额外存储结构,复杂度上升)
- 加一层本地缓存兜底:用
apcu_inc()快速计数,仅当接近阈值时才查Redis,减少穿透压力
真正麻烦的是混合代理环境下的IP还原,还有Lua脚本在Redis集群模式下的key哈希冲突——这两个点不踩一次很难意识到。











