限流是高并发系统的第一道防线。突发流量、恶意刷接口、下游服务过载等场景下,限流器能保护系统不被压垮。常见的限流算法有令牌桶(Token Bucket)、滑动窗口(Sliding Window)、漏桶(Leaky Bucket)三种,各有适用场景。Go语言的Goroutine和channel特性使其特别适合实现高性能并发限流器。
三种限流算法原理对比
令牌桶:以固定速率往桶中放令牌,桶有容量上限,满了则丢弃多余令牌。请求到来时从桶中取一个令牌,取到则放行,取不到则拒绝。令牌桶允许突发流量——桶满时积攒的令牌可以被一次性消耗,短时间内通过速率可以超过限制速率。
漏桶:请求像水滴一样滴入桶中,桶以固定速率漏出请求处理。桶满时新请求溢出丢弃。漏桶强制输出速率恒定,不允许突发。适合保护下游服务,确保不会被瞬时流量冲击。
滑动窗口:将时间划分为小窗口,统计当前窗口内的请求数。相比固定窗口,滑动窗口通过多窗口加权避免了窗口边界的突发问题。精度越高窗口越多,但内存开销也越大。
Go实现令牌桶限流器
以下是基于sync.Mutex和time.Ticker的令牌桶实现,支持突发流量:
package ratelimit
import (
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
rate float64 // 每秒生成令牌数
burst float64 // 桶容量(最大突发量)
tokens float64 // 当前令牌数
lastUpdate time.Time // 上次更新时间
}
func NewTokenBucket(rate, burst float64) *TokenBucket {
return &TokenBucket{
rate: rate,
burst: burst,
tokens: burst, // 初始满桶
lastUpdate: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
// 计算从上次到现在经过的时间,补充令牌
elapsed := now.Sub(tb.lastUpdate).Seconds()
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.burst {
tb.tokens = tb.burst // 不超过桶容量
}
tb.lastUpdate = now
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
func (tb *TokenBucket) Wait(n int) {
for i := 0; i < n; i++ {
for !tb.Allow() {
time.Sleep(10 * time.Millisecond)
}
}
}
实际使用中,更多推荐使用官方库golang.org/x/time/rate,它已经实现了高效的令牌桶算法:
package main
import (
"fmt"
"golang.org/x/time/rate"
"time"
)
func main() {
// 每秒10个令牌,桶容量20(允许20个突发)
limiter := rate.NewLimiter(10, 20)
for i := 0; i < 50; i++ {
// 非阻塞模式
if limiter.Allow() {
fmt.Printf("请求 %d: 通过\n", i+1)
} else {
fmt.Printf("请求 %d: 被限流\n", i+1)
}
time.Sleep(50 * time.Millisecond)
}
// 阻塞模式:等待直到获取令牌
limiter.Wait(context.Background())
}
滑动窗口限流器实现
滑动窗口使用Redis + Lua脚本实现分布式限流,以下是基于Redis的滑动窗口实现:
-- sliding_window_ratelimit.lua
-- KEYS[1]: 限流key
-- ARGV[1]: 窗口大小(秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(微秒)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max_requests = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window * 1000000
-- 清除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
-- 获取当前窗口内的请求数量
local count = redis.call('ZCARD', key)
if count >= max_requests then
return 0 -- 限流
end
-- 记录本次请求
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window + 1)
return 1 -- 放行
package ratelimit
import (
"context"
"github.com/redis/go-redis/v9"
"time"
)
type SlidingWindowLimiter struct {
client *redis.Client
script *redis.Script
window int // 窗口大小(秒)
maxRequests int // 窗口内最大请求数
}
var luaScript = redis.NewScript(`
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max_requests = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window * 1000000
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count >= max_requests then
return 0
end
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window + 1)
return 1
`)
func NewSlidingWindowLimiter(client *redis.Client, window, maxRequests int) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
client: client,
script: luaScript,
window: window,
maxRequests: maxRequests,
}
}
func (s *SlidingWindowLimiter) Allow(ctx context.Context, key string) bool {
now := time.Now().UnixMicro()
result, err := s.script.Run(ctx, s.client,
[]string{key},
s.window, s.maxRequests, now,
).Int()
if err != nil {
// Redis故障时降级放行,避免限流器故障导致服务不可用
return true
}
return result == 1
}
Gin框架中间件集成限流器
限流器通常以中间件形式集成到HTTP框架中。以下是基于Gin框架的限流中间件,支持按IP限流和按API路径限流两种维度:
package middleware
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// 每个IP一个独立的限流器
type IPRateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.Mutex
rate rate.Limit
burst int
}
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: r,
burst: b,
}
}
func (i *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter, exists := i.limiters[ip]
if !exists {
limiter = rate.NewLimiter(i.rate, i.burst)
i.limiters[ip] = limiter
}
return limiter
}
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc {
ipLimiter := NewIPRateLimiter(r, b)
return func(c *gin.Context) {
ip := c.ClientIP()
limiter := ipLimiter.getLimiter(ip)
if !limiter.Allow() {
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", b))
c.Header("Retry-After", "1")
c.JSON(http.StatusTooManyRequests, gin.H{
"code": 429,
"message": "请求过于频繁,请稍后再试",
})
c.Abort()
return
}
c.Next()
}
}
// 使用
func main() {
r := gin.Default()
// 每IP每秒10个请求,突发20个
r.Use(RateLimitMiddleware(10, 20))
r.GET("/api/users", getUsers)
r.Run(":8080")
}
生产环境中map会随IP增长无限膨胀,需要定期清理不活跃的限流器。可以用sync.Map配合后台goroutine定期清理,或改用LRU Cache限制最大数量。
分布式限流的设计考量
单机限流无法覆盖多实例部署场景。分布式限流通常基于Redis实现,但引入了网络延迟和单点依赖问题。设计时需要考虑:
降级策略:Redis不可用时的行为。常见方案是降级为本地限流,使用略宽松的阈值,宁可多放也不要因为限流器故障导致全部拒绝。
缓存一致性:本地缓存一份限流配置,定期从配置中心拉取更新。避免每次请求都查询Redis配置信息。
热点Key问题:大量请求集中打到一个Redis Key上,导致单个Redis节点压力过大。可通过Key分片(如按用户ID哈希分散到不同Key)或Redis Cluster来解决。
性能优化:Lua脚本在Redis中原子执行,减少网络往返。批量请求可以用pipeline合并。对于超高并发场景,可以用Redis Cluster + 本地预扣减的双层限流,本地先快速预扣,定期与Redis同步,将大部分请求拦截在本地,减少Redis访问量。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-shi-xian-gao-bing-fa-xian-liu-qi-ling-pai-tong-yu/