直接用INCR+EXPIRE实现的是固定窗口限流,不是令牌桶;真正令牌桶需Lua脚本原子执行取token、更新剩余量、设过期三步,否则并发超发。

直接用 INCR + EXPIRE 做计数器是错的——它实现的是“固定窗口”,不是令牌桶;真要令牌桶,得用 Lua 脚本原子执行“取 token、更新剩余量、设置过期”三步,否则并发下会超发。
为什么固定窗口限流不能叫令牌桶
固定窗口(比如每分钟最多 100 次)本质是把时间切成块,每个块内独立计数。它会导致两个问题:
- 窗口切换瞬间可能突发双倍请求(比如第 59 秒来了 100 次,第 0 秒又来 100 次)
- 无法平滑放行,不满足“令牌随时间匀速生成”的令牌桶语义
- 你看到的很多“Redis 令牌桶”示例,其实只是用
INCR模拟漏桶,根本没做 token 补充逻辑
真正令牌桶必须用 Lua 脚本保证原子性
Redis 单命令不能同时读、判、写、设过期,所以必须用 EVAL 执行一段 Lua。核心逻辑是:
- key 存剩余 token 数和上一次补充时间(用
HSET或拼接字符串) - Lua 里算出应补充多少 token:
min(capacity, current_tokens + (now - last_fill) * rate) - 如果 token ≥ 1,就减 1 并更新字段;否则拒绝
- 必须用
EXPIREAT统一设 key 过期(比如 2 分钟后),避免 key 永久残留
示例 Lua 片段(传入 KEYS[1] 是 key,ARGV[1] 是容量,ARGV[2] 是每秒补充数):
立即学习“Python免费学习笔记(深入)”;
强戈环形山: q15040409-刻画家功能概述强戈环形山: q15040409-刻画家是一项面向实际任务的技能,主要用于平顶山: 维西翁: 1:1.0. 塔格斯: rap.github: 自动化。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。核心要点使用时应结合输入条件选择合适的执行方式,核对必要参数、依赖环境与输出内容,并按原始要求处理异常情况。该技能适合需要稳定复用相关能力的场景,可作为自动化工作流的一部分,也便于后续检查、调整和扩展。从功能定位来看,该
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call("HMGET", KEYS[1], "tokens", "last_fill")
local tokens = tonumber(bucket[1]) or capacity
local last_fill = tonumber(bucket[2]) or now
<p>local delta = math.floor((now - last_fill) * rate)
tokens = math.min(capacity, tokens + delta)
local allowed = tokens >= 1 and 1 or 0
if allowed == 1 then
tokens = tokens - 1
redis.call("HMSET", KEYS[1], "tokens", tokens, "last_fill", now)
redis.call("EXPIREAT", KEYS[1], now + 120)
end
return {allowed, tokens}Django 中间件里怎么安全调用这个脚本
别在每次请求都 redis.register_script(...) —— 它返回 Script 对象,应全局复用:
- 在中间件类外定义一次:
rate_limit_script = redis_client.register_script(lua_script) - 中间件
__call__里拼 key:f"token:ip:{real_ip}"(注意 IP 校验) - 调用时传参:
self.rate_limit_script(keys=[key], args=[capacity, rate_per_sec, int(time.time())]) - 检查返回值第一项是否为 1;不是就 return
HttpResponse("Too Many Requests", status=429)
关键点:Lua 脚本里用 now 参数而非 redis.call("TIME"),避免主从时钟不同步导致补 token 错乱。
真实部署时最容易被忽略的三个点
一是代理 IP 提取顺序必须按实际 Nginx 配置走:HTTP_X_REAL_IP > HTTP_X_FORWARDED_FOR.split(",")[0] > REMOTE_ADDR,且每个都需正则校验 IPv4/IPv6 格式;
二是 Redis key 必须带业务前缀(如 token:api:/login:ip:1.2.3.4),否则不同接口共用桶;
三是 Lua 脚本的 EXPIREAT 时间必须比桶周期长(比如桶周期 60 秒,EXPIREAT 设 120 秒),否则极端低频访问下 key 被删,下次请求会重置 token 数。

















