← 中文资源

简体中文版本

Fiber 滑动窗口限流中间件

为 Fiber 实现滑动窗口限流中间件:按 API key 或 IP 取键、窗口内过期事件就地产出、超限返回 429 与重试秒数。

阅读英文原文

示例说明

固定窗口在边界处会双倍放行——窗口切换瞬间两倍配额。滑动窗口按时间点计数:判定时先剔除窗口外的历史事件再比较余量,任意时刻向前看一分钟都精确。中间件形态让全部路由共享一把尺子,健康探针显式豁免防止编排器探活误耗配额。

技术要点

  • 滑动窗口消除边界双倍放行
  • 按 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"))
}