限流器必须基于redis实现中心化限流,hertz单机限流在多实例下失效;需用go-redis/v9配合lua脚本保证原子性,键按ip/uid/path等维度设计,脚本须返回是否允许及剩余次数。

限流器必须走 Redis,不能只靠 Hertz 中间件
单机限流在微服务里基本没用——Hertz 启动多个实例后,每个实例各自计数,总请求数轻松突破阈值。真正起作用的限流器必须依赖中心化存储,Redis 是目前最轻量、最可靠的选择。别试图在 Hertz 的 middleware 里用 sync.Map 或 atomic 做全局计数,那只是自欺欺人。
用 go-redis/v9 + Lua 脚本实现原子性限流
Redis 命令本身不是原子的(比如先 GET 再 INCR 再 EXPIRE),并发请求下容易超限。必须用 Lua 脚本把逻辑打包进一次 EVAL 执行。go-redis/v9 支持直接传入脚本,不用自己拼 redis-cli 命令。
- 键名建议按维度设计,例如:
rate:ip:{client_ip}、rate:uid:{user_id}、rate:path:{full_path} - Lua 脚本要返回两个值:是否允许(0/1)和当前剩余次数(便于日志或响应头透出)
- 不要用
SET key val EX sec配合INCR,这种组合在高并发下会漏判;必须用redis.call("INCR", KEYS[1])+redis.call("EXPIRE", KEYS[1], ARGV[1])在同一脚本里完成 - 示例脚本(固定窗口):
if redis.call("INCR", KEYS[1]) == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
local current = tonumber(redis.call("GET", KEYS[1]))
if current
<h3>Hertz 中间件里如何安全调用限流逻辑</h3>
<p>别在中间件里每次请求都新建 <code>*redis.Client</code>,连接池必须复用。初始化时通过 <code>hertz.Engine.Use()</code> 注入已配置好的限流器实例,而不是在 <code>func(ctx context.Context, c *app.RequestContext)</code> 里临时初始化。</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>
- 限流器结构体里持有
*redis.Client和限流参数(limit、window),避免每次调用都传参 - 中间件中提取 key 的逻辑要提前判断边界:空 IP、非法 user_id、未登录用户 fallback 到 IP 维度等
- 遇到
redis.Nil错误是正常情况(key 不存在),别当异常 panic;其他错误(如网络断开)才该记录 error 日志并考虑降级(比如放行或返回 503) - 不要阻塞等待 Redis 响应太久,给
context.WithTimeout设个硬上限,比如 100ms,超时直接拒绝(比让请求卡住强)
滑动窗口限流比固定窗口更准,但代价更高
固定窗口简单高效,但存在“窗口跳跃”问题(比如限 100 次/分钟,用户在第 59 秒发 100 次,第 60 秒又发 100 次)。滑动窗口能解决,但要用 ZSET 存时间戳,每次 ZREMRANGEBYSCORE + ZCARD,吞吐量明显下降。
- 如果业务对精度要求不高(如后台管理接口),用固定窗口 + 合理缩短窗口时间(如 10 秒)更稳妥
- 如果必须滑动(如支付回调、短信发送),建议用 RedisTimeSeries 或单独部署 Redis Stack,原生
ZSET在万级 QPS 下延迟抖动明显 - 别在 Lua 里遍历
ZSET——ZCOUNT或ZCARD就够用,遍历操作会把 Redis 线程卡死
真正难的不是写几行限流代码,而是决定「按什么维度限」和「超限时返回什么」。IP 维度容易被代理穿透,UID 维度依赖登录态,路径维度可能漏掉 query 参数差异。这些逻辑不在 Redis 里,而在你提取 key 的那段 if-else 里。










