直接用 incr 不能可靠限流,因为 incr 与 expire 分两步执行存在竞态:服务崩溃或连接中断会导致 key 永不过期;多客户端并发时易漏设过期时间或重复设置;唯一可靠方案是用 lua 脚本原子封装 incr 和 expire。

为什么直接用 INCR 不能可靠限流
因为单次 INCR 没有原子性过期控制:你先 INCR,再 EXPIRE,中间若服务崩溃或 Redis 连接中断,key 就永远不带过期时间,导致后续所有请求被误拒。更糟的是,多个客户端并发执行这两步,可能重复设置过期时间或漏设。
正确做法是用 INCR + EXPIRE 的原子组合,但 Redis 原生命令不支持。所以必须用 Lua 脚本封装——它在服务端原子执行,且可返回计数值供 Go 判断是否超限。
示例 Lua 脚本(保存为 rate_limit.lua):
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
return current
Go 中调用 Lua 脚本实现限流的最小可行代码
使用 github.com/go-redis/redis/v9(v9 是当前主流),关键不是写脚本,而是安全加载、传参、解析返回值。
-
script.Load()只需执行一次(如init()或启动时),避免每次重复加载开销 -
script.Eval()的keys参数必须是非空切片(哪怕只有一个 key),否则 Redis 报错ERR wrong number of arguments -
ARGV[1]对应限流窗口秒数(如 60),必须转成字符串传入,Lua 里再tonumber() - 返回值是
int64,直接和限流阈值(如 100)比较即可
实操代码片段:
var rateLimitScript = redis.NewScript(`
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
return current
`)
<p>func isAllowed(ctx context.Context, client *redis.Client, key string, windowSec int, limit int) (bool, error) {
result, err := rateLimitScript.Eval(ctx, client, []string{key}, strconv.Itoa(windowSec)).Int64()
if err != nil {
return false, err
}
return int(result) </p><h3>Key 设计决定限流粒度,别硬编码用户 ID</h3><p>限流效果完全取决于 key 的构造逻辑。常见错误是把 key 写死成 <code>"rate:limit"</code>,导致全站共用一个计数器;或者只用 <code>userID</code>,忽略接口路径、IP、HTTP 方法等维度。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理"><img
src="https://img.php.cn/upload/skill/000/000/081/178960683454849.jpg" alt="Redis Skill - 高性能缓存管理" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="overflowclass">Redis Skill - 高性能缓存管理</a>
<p class="overflowclass">Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>推荐按场景组合 key:</p>
- 接口级限流:
"rl:" + path(如"rl:/api/pay") - 用户+接口:
"rl:" + userID + ":" + path(注意防注入,userID需校验格式) - IP+接口:
"rl:" + getClientIP(r) + ":" + r.URL.Path(需清洗 IP,如 IPv6 标准化、隐藏私有地址)
不要在 key 里拼毫秒时间戳——这会让过期逻辑失效;Redis 的 EXPIRE 是相对当前时间的,脚本里已处理。
超限时该返回什么 HTTP 状态码和 Header
限流不是报错,是正常业务响应。别返回 503 Service Unavailable,标准是 429 Too Many Requests。
必须带上 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 这三个 Header,否则前端或网关无法友好重试。其中 Reset 是 Unix 时间戳(秒级),可用 time.Now().Add(time.Duration(windowSec) * time.Second).Unix() 计算。
注意:Go 的 http.ResponseWriter 不允许在 Write 后再 SetHeader,务必在 w.WriteHeader() 前写完所有 Header。
容易被忽略的一点:如果限流 key 是基于 IP 的,而你的服务在 Nginx 后,RemoteAddr 拿到的是 Nginx 的内网地址,必须从 X-Forwarded-For 解析真实 IP,并做可信代理白名单校验,否则 header 可被伪造。










