Go语言实现高并发限流器:令牌桶与滑动窗口算法对比

限流是高并发系统的第一道防线。突发流量、恶意刷接口、下游服务过载等场景下,限流器能保护系统不被压垮。常见的限流算法有令牌桶(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/

(0)
小编小编
上一篇 14小时前
下一篇 14小时前

相关推荐