示例说明
固定窗口在边界处会双倍放行——窗口切换瞬间两倍配额。滑动窗口按时间点计数:判定时先剔除窗口外的历史事件再比较余量,任意时刻向前看一分钟都精确。中间件形态让全部路由共享一把尺子,健康探针显式豁免防止编排器探活误耗配额。
技术要点
- 滑动窗口消除边界双倍放行
- 按 API key 优先的分层取键
- 探针路径免限流防误伤
代码解读
slidingWindow 的 allow 先做窗口裁剪——把过期时间点就地压缩掉,保留的计数即当前窗口余量;超限时返回最早事件加窗口长的重试时间。Fiber 中间件里先取键再判定,429 响应带 Retry-After 头与结构化错误体。第二段注册演示官方 limiter 中间件的全局兜底。
使用建议
内存版仅单实例有效,多实例配 Redis 时间点。事件切片要防长尾膨胀,可加定期清扫。突发接口可配初始突发额度。
代码示例
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
)
type slidingWindow struct {
mu sync.Mutex
window time.Duration
limit int
events map[string][]time.Time
}
func (s *slidingWindow) allow(key string, now time.Time) (bool, time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
cutoff := now.Add(-s.window)
times := s.events[key]
kept := times[:0]
for _, t := range times {
if t.After(cutoff) {
kept = append(kept, t)
}
}
if len(kept) >= s.limit {
s.events[key] = kept
return false, time.Until(kept[0].Add(s.window))
}
s.events[key] = append(kept, now)
return true, 0
}
func limiterMiddleware(window time.Duration, limit int) fiber.Handler {
sw := &slidingWindow{window: window, limit: limit, events: map[string][]time.Time{}}
return func(c *fiber.Ctx) error {
key := c.Get("X-API-Key")
if key == "" {
key = c.IP()
}
if c.Path() == "/healthz" {
return c.Next() // 探针不限流
}
ok, retryIn := sw.allow(key, time.Now())
if !ok {
c.Set("Retry-After", fmt.Sprintf("%.0f", retryIn.Seconds()))
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "rate limit exceeded",
"retry_s": retryIn.Seconds(),
})
}
return c.Next()
}
}
func main() {
app := fiber.New()
app.Use(limiterMiddleware(time.Minute, 100))
app.Use(limiter.New(limiter.Config{Max: 500, Expiration: time.Minute}))
app.Get("/data", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true})
})
app.Get("/healthz", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "up"})
})
log.Fatal(app.Listen(":3000"))
}