直接用 time.Now() 无法实现限流,因缺少状态存储;内存版需用 sync.Map 存 IP 时间戳切片并滑动清理;多实例必须用 Redis+Lua 原子操作;注意代理 IP、静态资源跳过及 Retry-After 头。

为什么不能直接用 time.Now() 做限流判断
在 Fiber 中间件里写 time.Now() 获取当前时间本身没问题,但仅靠它无法实现“单位时间窗口内限制请求数”——因为缺少状态存储。每次请求进来都是全新执行,上一次的计数、时间戳全丢了。你得把访问记录存起来,并在下次请求时读取、比对、更新。
内存版 IP 限流中间件怎么写
适合单实例部署、QPS 不高(比如内部工具 API)的场景。核心是用 sync.Map 存每个 IP 的访问时间切片,按窗口滑动清理过期条目。
- 用
sync.Map而非普通map,避免并发写 panic - 每个 IP 对应一个
[]int64,存 Unix 时间戳(秒级足够,别用纳秒) - 每次请求先清理该 IP 列表中早于
now - window的时间戳 - 清理后若长度 ≥ 限制数(如 10),直接
ctx.Status(429).SendString("Too many requests")并return - 否则追加当前时间戳,再调
ctx.Next()
示例关键逻辑:
var ipLimit = sync.Map{} // key: ip, value: []int64
app.Use(func(c *fiber.Ctx) error {
ip := c.IP()
window := 60 // 秒
maxReq := 10
<pre class='brush:php;toolbar:false;'>if v, ok := ipLimit.Load(ip); ok {
times, _ := v.([]int64)
now := time.Now().Unix()
// 清理过期时间戳
clean := make([]int64, 0, len(times))
for _, t := range times {
if now-t < int64(window) {
clean = append(clean, t)
}
}
if len(clean) >= maxReq {
return c.Status(fiber.StatusTooManyRequests).SendString("Too many requests")
}
clean = append(clean, now)
ipLimit.Store(ip, clean)
} else {
ipLimit.Store(ip, []int64{time.Now().Unix()})
}
return c.Next()})
多实例部署必须用 Redis 实现共享状态
K8s 多副本或多个进程时,内存版会失效:每个实例维护自己的 sync.Map,限流变成“每台机器各自算”,总流量翻倍突破阈值。必须换 Redis。
- 用
github.com/go-redis/redis/v9客户端,连接池复用 - Key 设计推荐:
rate_limit:ip:{ip}:ts,用LPUSH + LTRIM维护时间戳列表 - 注意:Redis 命令要带超时(
SETEX或设置 key 过期),否则旧数据堆积 - 别用
INCR + EXPIRE两步操作——不是原子的,高并发下可能漏计数;改用 Lua 脚本封装
典型 Lua 脚本(用于 Eval):
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
<p>-- 清理过期时间戳
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)</p><p>-- 获取当前数量
local count = redis.call("ZCARD", key)</p><p>if count >= max then
return 0
end</p><p>-- 添加当前时间戳(用 score 排序)
redis.call("ZADD", key, now, now)
redis.call("EXPIRE", key, window + 1)
return 1容易被忽略的边界问题
限流不是加个中间件就完事。真实线上常踩的坑包括:
-
c.IP()可能拿到反向代理的内网 IP,得配app.Config().ProxyHeader = "X-Forwarded-For"并启用fiber.New(&fiber.Config{...})的信任代理设置 - 前端 SPA 刷新页面会触发多次请求(JS 加载、favicon、manifest),别让这些干扰业务限流,建议对
/static/、/favicon.ico跳过中间件 - 返回 429 时,最好带上
Retry-After响应头,告诉客户端多久后重试,否则前端可能立即重发造成雪崩 - 测试时用
curl -H "X-Forwarded-For: 1.2.3.4"模拟不同 IP,别只测 localhost


















