用 incr + expire 实现原子计数与过期,key 为 rate:ip:{ip}:{ts_bucket}(ts_bucket = int(time.time() // 60)),expire 设为 65 秒;真实 ip 需优先取 http_x_real_ip 或安全解析 http_x_forwarded_for,并校验格式;避免第三方包因分布式不一致与非原子操作导致限流失效。

怎么用 Redis 实现单 IP 每分钟最多 100 次请求的硬限制
直接上核心逻辑:用 INCR + EXPIRE 组合,而不是先 GET 再判断再 SET——后者在并发下会漏判。Django 中间件里对每个请求提取 request.META.get('REMOTE_ADDR')(注意代理场景要改用 X-Forwarded-For),拼成 Redis key,比如 f"rate:ip:{ip}:20240520:12"(按小时分桶)或更细粒度的 f"rate:ip:{ip}:{int(time.time() // 60)}"(每分钟一桶)。
常见错误是把时间戳直接塞进 key 后不清理旧桶,导致 Redis 内存无限涨;或者用 SETNX + 过期时间,但无法做计数累加。
- 推荐用
INCR命令,它天然原子,返回值就是当前计数值 - 紧接着调用
EXPIRE设置过期(注意:如果 key 已存在且已有过期时间,EXPIRE不会重置,得用EXPIREAT或确保首次设置) - 如果返回值 > 100,立刻
return HttpResponse("Too Many Requests", status=429) - 别用
time.time()算 key,用int(time.time() / 60) * 60对齐整分钟,避免同一分钟内生成多个 key
Django 中间件里怎么安全取真实客户端 IP
直接读 request.META['REMOTE_ADDR'] 在 Nginx 反向代理后大概率拿到的是 127.0.0.1,不是用户真实 IP。必须根据你实际部署结构决定怎么取:
- 如果你的 Nginx 配置了
proxy_set_header X-Forwarded-For $remote_addr;,且没被多层代理污染,可用request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() - 如果用了 Cloudflare 或阿里云 SLB,它们会加
X-Real-IP,优先读这个:request.META.get('HTTP_X_REAL_IP') - 永远对取出的 IP 做基础校验(比如正则匹配 IPv4/IPv6 格式),防止 header 注入伪造 key
- 千万别信任
HTTP_X_FORWARDED_FOR整个字符串——攻击者可以手动加逗号伪造“多层代理”,绕过首项截取
为什么不用 Django-Ratelimit 这类第三方包
不是不能用,而是它默认基于 Django cache(可能走数据库或本地内存),在分布式部署下失效;即使配 Redis backend,它的 key 设计和过期策略也不够紧凑,比如默认用完整 URL 路径做 key 前缀,导致相同 IP 访问不同接口也共用计数器,不符合“单接口频控”需求。
立即学习“Python免费学习笔记(深入)”;
你自己写中间件能精确控制三点:key 结构(rate:ip:{ip}:path:{view_name})、时间窗口对齐方式、触发阈值响应行为(比如记录日志、返回特定 header、跳转到验证码页)。
-
Django-Ratelimit的group参数可以定制 key,但文档模糊,容易误配成全局共享计数 - 它内部用
cache.set(key, value, timeout),而set不是原子递增,高并发下计数不准 - 它不暴露底层 Redis 连接,调试时没法直接
redis-cli monitor看 key 写入情况
Redis key 设计和内存泄漏风险怎么防
key 乱起名 + 不设 TTL = Redis 内存缓慢爬升,直到 OOM。必须让每个 key 天然带过期语义,而不是靠后台定时清理。
- 不要用日期字符串(如
"2024-05-20")当 key 后缀——每天要手动删旧 key,运维成本高 - 推荐用时间戳桶:key 是
f"rate:ip:{ip}:{ts_bucket}",其中ts_bucket = int(time.time() // 60),然后EXPIRE key 65(比窗口长 5 秒,覆盖边界请求) - 避免在 key 里塞 session_id、user_id 等动态长字符串,IP + 时间桶已足够区分,长度可控
- 上线前用
redis-cli --scan --pattern "rate:ip:*" | wc -l估算 key 规模:1 万 IP × 每分钟 1 key = 每分钟 1 万 key,60 分钟约 60 万,Redis 完全扛得住;但要是每秒一桶,就变成 3600 万,得重新评估
真正麻烦的是测试阶段本地没配好代理头,IP 全是 127.0.0.1,结果所有请求挤在一个 key 下,你以为限流生效了,其实是误伤。上线前一定用 curl 加 -H "X-Real-IP: 1.2.3.4" 过一遍链路。










