update
This commit is contained in:
97
internal/middleware/ratelimit.go
Normal file
97
internal/middleware/ratelimit.go
Normal file
@ -0,0 +1,97 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fashionapi/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// rateLimiter 基于令牌桶的按 IP 限流(进程内,单实例足够;多实例需换集中式如 redis)。
|
||||
//
|
||||
// 设计取舍:公开接口「首页公开、翻页需登录」已天然抬高批量枚举门槛;本中间件作为
|
||||
// 兜底,按客户端 IP 限制总量,兜住 casual 爬虫与突发流量,避免单 IP 打垮服务。
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
visitors map[string]*visitor
|
||||
rate float64 // 每秒补充令牌数
|
||||
burst float64 // 桶容量
|
||||
lastSeen map[string]time.Time
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
tokens float64
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func newRateLimiter(rps, burst int) *rateLimiter {
|
||||
rl := &rateLimiter{
|
||||
visitors: make(map[string]*visitor),
|
||||
lastSeen: make(map[string]time.Time),
|
||||
rate: float64(rps),
|
||||
burst: float64(burst),
|
||||
}
|
||||
go rl.cleanup(10 * time.Minute)
|
||||
return rl
|
||||
}
|
||||
|
||||
// cleanup 周期清理长时间无活动的客户端,避免 map 无限增长。
|
||||
func (rl *rateLimiter) cleanup(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
for ip, t := range rl.lastSeen {
|
||||
if time.Since(t) > 3*interval {
|
||||
delete(rl.visitors, ip)
|
||||
delete(rl.lastSeen, ip)
|
||||
}
|
||||
}
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimiter) allow(ip string) bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
now := time.Now()
|
||||
v, ok := rl.visitors[ip]
|
||||
if !ok {
|
||||
// 首次访问直接发放 burst-1 个令牌(允许一个突发请求),后续按速率补充
|
||||
rl.visitors[ip] = &visitor{tokens: rl.burst - 1, last: now}
|
||||
rl.lastSeen[ip] = now
|
||||
return true
|
||||
}
|
||||
elapsed := now.Sub(v.last).Seconds()
|
||||
v.tokens += elapsed * rl.rate
|
||||
if v.tokens > rl.burst {
|
||||
v.tokens = rl.burst
|
||||
}
|
||||
v.last = now
|
||||
rl.lastSeen[ip] = now
|
||||
if v.tokens >= 1 {
|
||||
v.tokens--
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RateLimit 按 IP 的速率限制中间件。Enabled=false 时为 noop(便于灰度与本地开发)。
|
||||
func RateLimit(cfg config.RateLimitConfig) gin.HandlerFunc {
|
||||
if !cfg.Enabled || cfg.RPS <= 0 {
|
||||
return func(c *gin.Context) { c.Next() }
|
||||
}
|
||||
rl := newRateLimiter(cfg.RPS, cfg.Burst)
|
||||
return func(c *gin.Context) {
|
||||
if !rl.allow(c.ClientIP()) {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "too many requests",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user