不能只用 incr + expire 组合,因高并发下存在竞态:两请求同时发现 key 不存在,均执行 incr 得 1,若首个请求未及 expire 时第二个已完成 incr,则 key 无 ttl 而永久残留;且 redis 6.0 的 incr ex 只在 key 不存在时生效,已存在时不更新 ttl,导致限流失效。

为什么不能只用 INCR + EXPIRE 组合
INCR 和 EXPIRE 分开调用在高并发下会出竞态:两个请求几乎同时发现 key 不存在,都执行 INCR 得到 1,又都执行 EXPIRE——表面看没问题,但若第一个请求 INCR 后还没来得及 EXPIRE,第二个请求就 INCR 完了,此时 key 没设 TTL,会永久残留,变成“死计数器”。更隐蔽的问题是:Redis 6.0 虽支持 INCR key EX 60,但它只在 key 不存在时生效;key 已存在时,TTL 不更新,上一窗口的过期时间可能早已失效,导致限流失效。
必须用 Lua 脚本原子化完成三件事
一个生产可用的 IP 防刷脚本,必须在一个 EVAL 或 register_script 调用中完成:
-
INCR当前计数 - 判断返回值是否为
1(即首次访问) - 仅在此时调用
EXPIRE设 TTL(如 60 秒)
示例脚本(推荐预注册):
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
调用时传入 KEYS[1](如 ip:192.168.1.100)和 ARGV[1](如 60)。注意:不要用 redis.call("TIME") 获取时间,它精度只有秒级,且各节点时钟不同步,此处不需要时间戳。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
真实客户端 IP 怎么取才不被伪造
直接读 request.client.host 在 Nginx / Cloudflare 后几乎总是拿到代理 IP。必须按优先级检查 header:
-
X-Real-IP(Nginx 默认透传,最可靠) -
X-Forwarded-For(取最左非内网 IP,需配合可信代理白名单) -
CF-Connecting-IP(Cloudflare 场景下必须启用)
FastAPI 中应写成:request.headers.get("X-Real-IP") or request.headers.get("X-Forwarded-For", "").split(",")[0].strip(),并确保反向代理配置了 set_real_ip_from。
怎么避免脚本注册后执行报 NOSCRIPT
NOSCRIPT No matching script 错误通常不是脚本没上传,而是连接错乱:你在连接 A 上调用 register_script,却用连接池里新取的连接 B 去执行 script.execute(...)。Redis 的脚本 SHA1 是按连接实例缓存的。
- 用
redis-py时,确保register_script返回的脚本对象在同一个连接实例或连接池生命周期内复用 - 不要跨线程/协程共享未加锁的连接对象
- 若用连接池,建议在应用启动时统一注册,脚本对象全局单例
真正容易被忽略的是:脚本里没做限流阈值判断——INCR 只负责计数,是否拦截必须由业务代码根据返回值决定,Lua 本身不拒绝请求,它只保证计数和过期逻辑不被并发破坏。










