INCR配合EXPIRE是最稳的限流组合,因INCR原子性可避免并发漏统计;需先INCR再立即EXPIRE,推荐Redis 7.0+使用INCR key EX 60一步到位。

用 INCR + EXPIRE 做基础限流,别直接用 SETNX
Redis 防刷本质是「单位时间窗口内计数」,INCR 配合 EXPIRE 是最稳的组合。很多人图省事用 SETNX 判断是否存在再自己加逻辑计数,结果在并发下漏统计、超限放行——因为 SETNX 和后续 INCR 不是原子的。
正确做法:先 INCR,再检查返回值是否为 1(说明是窗口内第一次请求),立刻跟 EXPIRE 设置过期。哪怕并发写入,INCR 本身是原子的,不会丢计数。
-
INCR返回值就是当前累计值,直接比对阈值,不用额外GET - 必须在
INCR后立即EXPIRE,否则首次请求可能没过期时间,下次窗口重叠时误判 - 如果用 pipeline 批量操作,确保
INCR和EXPIRE在同一个 pipeline 里,但注意 Redis 7.0+ 支持INCR的EX选项,可一步到位:INCR key EX 60
接口粒度怎么定:用 IP 还是 token 还是 user_id
防刷不是越细越好,得看攻击面和业务容忍度。纯按 IP 容易误伤(NAT、运营商共享 IP);只按 user_id 对未登录用户无效;全靠前端传 token 又容易被绕过。
推荐分层设计:
- 未登录场景:用
IP+User-Agent+ 请求路径拼接 key,比如rate:ipua:/api/login:123.45.67.89:Mozilla/5.0,降低共享 IP 误杀率 - 已登录场景:优先用
user_id,后端从 token 或 session 解出,不依赖前端传参 - 关键操作(如发短信、删账号):叠加校验,比如
user_id+ 操作类型 + 设备指纹 hash,key 如rate:action:sms:u123:abcde
EVAL 脚本能解决原子性问题,但别滥用
当需要「判断+计数+设置过期+返回状态」一气呵成,又不想升级 Redis 版本,EVAL 是可靠选择。但它不是银弹:Lua 脚本执行期间会阻塞其他命令,长脚本或高频调用反而拖垮吞吐。
一个安全的限流脚本示例(限制 60 秒最多 10 次):
eval "local c = redis.call('incr', KEYS[1]); if c == 1 then redis.call('expire', KEYS[1], ARGV[1]) end; if c > tonumber(ARGV[2]) then return 0 else return c end" 1 rate:ip:1.2.3.4 60 10
- 脚本里必须用
redis.call(),不能用redis.pcall()(后者不抛错,掩盖逻辑问题) - KEYS 和 ARGV 严格分离,避免 Lua 注入风险(虽然 Redis 本身不解析参数,但拼接字符串构造 key 仍危险)
- 上线前用
SCRIPT LOAD预加载,再用EVALSHA调用,减少网络传输和解析开销
穿透防护要配合业务层,光靠 Redis 不够
防刷拦截的是「高频请求」,但缓存穿透是「查不到的数据反复打进来」。两者常一起发生——攻击者故意用随机 ID 刷 /user?id=xxx,Redis 没命中就直击 DB。
单纯在 Redis 层限流治标不治本:
- 对空结果也要缓存(布隆过滤器 or
SET记录已确认不存在的 ID),否则限流只拦住频率,拦不住穿透本身 - 限流 key 必须包含业务标识,比如
/user?id=123和/user?id=456应该是不同 key,否则一个恶意 ID 就把整个接口锁死 - 日志里记录被限流的完整请求信息(IP、UA、path、query),否则你根本分不清是真实攻击还是前端 bug 导致的重试风暴
真正难的不是写几行 Redis 命令,而是想清楚这个接口谁会刷、为什么刷、刷成功了对业务影响多大——这些决定了 key 怎么设计、阈值设多少、要不要告警、要不要人肉审核。










