Go语言在高并发服务场景中对流量控制有强需求。限流器(Rate Limiter)保护后端服务不被突发流量压垮,是微服务治理的基础组件。Go标准库golang.org/x/time/rate提供了生产级的令牌桶实现,但实际业务中往往需要更灵活的多维度限流策略。本文从算法原理到工程实现,覆盖令牌桶、滑动窗口两种限流算法的Go语言实战。
令牌桶算法原理与标准库使用
令牌桶(Token Bucket)的核心机制:系统以固定速率向桶中放入令牌,桶有最大容量上限。每个请求到来时从桶中取走一个令牌,取到则放行,取不到则拒绝或排队。令牌桶的优势在于允许一定程度的突发流量——当桶中积攒了令牌时,可以实现短时高于平均速率的吞吐。
Go标准库的rate.Limiter使用:
package main
import (
"context"
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
// 创建限流器:每秒10个令牌,桶容量20
// rate.Limit(10) = 每秒10个请求
// 20 = 突发容量,允许一次最多20个请求通过
limiter := rate.NewLimiter(rate.Limit(10), 20)
for i := 0; i < 50; i++ {
// Wait方法:取不到令牌时阻塞等待
err := limiter.Wait(context.Background())
if err != nil {
fmt.Printf("请求 %d 被拒绝: %v\n", i, err)
continue
}
fmt.Printf("请求 %d 通过: %s\n", i, time.Now().Format("15:04:05.000"))
}
}
Allow方法适用于非阻塞场景,直接返回是否允许通过:
func handleRequest(limiter *rate.Limiter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Allow消耗一个令牌,返回false说明限流
if !limiter.Allow() {
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"code":429,"message":"请求过于频繁"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code":0,"message":"ok"}`))
}
}
基于Redis的分布式令牌桶实现
单机限流器在多实例部署时无法共享状态。基于Redis的分布式限流器通过Lua脚本保证原子性,所有实例共享同一计数桶:
package ratelimit
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type RedisTokenBucket struct {
client *redis.Client
rate int // 每秒令牌数
capacity int // 桶容量
key string // Redis key前缀
}
func NewRedisTokenBucket(client *redis.Client, rate, capacity int, key string) *RedisTokenBucket {
return &RedisTokenBucket{
client: client,
rate: rate,
capacity: capacity,
key: fmt.Sprintf("ratelimit:%s", key),
}
}
// Lua脚本保证获取令牌的原子性
const tokenBucketScript = `
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(data[1])
local last_time = tonumber(data[2])
-- 初始化
if tokens == nil then
tokens = capacity
last_time = now
end
-- 计算自上次请求以来新增的令牌数
local elapsed = now - last_time
local new_tokens = elapsed * rate / 1000
tokens = math.min(capacity, tokens + new_tokens)
-- 判断是否放行
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
-- 更新状态
redis.call('HMSET', key, 'tokens', tokens, 'last_time', now)
redis.call('EXPIRE', key, 60)
return allowed
`
func (r *RedisTokenBucket) Allow(ctx context.Context) (bool, error) {
now := time.Now().UnixMilli()
result, err := r.client.Eval(ctx, tokenBucketScript, []string{r.key},
r.rate, r.capacity, now, 1).Int()
if err != nil {
return false, err
}
return result == 1, nil
}
Lua脚本在Redis中以原子方式执行,避免了”先读取再写入”的竞态条件。EXPIRE设置60秒过期,防止无效key长期占用内存。参数中的now使用毫秒时间戳,保证多实例间时钟基本同步。对于精度要求更高的场景,可以引入NTP时间同步。
滑动窗口限流算法实现
滑动窗口(Sliding Window)相比固定窗口避免了临界点突发问题。核心思路是维护一个时间窗口内的请求计数,窗口随时间滑动。Redis的Sorted Set天然适合实现这个方案:
package ratelimit
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type SlidingWindowLimiter struct {
client *redis.Client
key string
limit int64 // 窗口内最大请求数
window time.Duration // 窗口大小
}
func NewSlidingWindowLimiter(client *redis.Client, key string, limit int64, window time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
client: client,
key: fmt.Sprintf("sliding:%s", key),
limit: limit,
window: window,
}
}
const slidingWindowScript = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]
-- 计算窗口起始时间
local window_start = now - window_ms
-- 移除窗口外的旧请求记录
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 添加当前请求,score为当前时间戳
redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, math.ceil(window_ms / 1000))
return 1
else
return 0
end
`
func (s *SlidingWindowLimiter) Allow(ctx context.Context) (bool, error) {
now := time.Now().UnixMilli()
// 使用纳秒时间戳保证member唯一性
member := fmt.Sprintf("%d-%d", now, time.Now().UnixNano())
result, err := s.client.Eval(ctx, slidingWindowScript, []string{s.key},
now, s.window.Milliseconds(), s.limit, member).Int()
if err != nil {
return false, err
}
return result == 1, nil
}
ZREMRANGEBYSCORE清理窗口外的历史请求,ZCARD统计当前窗口内的请求总数。每次请求将当前时间戳作为score写入Sorted Set,member使用纳秒级唯一标识避免覆盖。
HTTP中间件集成与多维度限流
实际业务中,限流维度通常不止一个——全局总流量、单IP频率、单用户频率需要组合控制。以下中间件支持多维度限流:
package middleware
import (
"context"
"net/http"
"strconv"
"sync"
"github.com/redis/go-redis/v9"
)
type RateLimitConfig struct {
GlobalRate int // 全局每秒请求数
GlobalCap int
IPRate int // 单IP每秒请求数
IPCap int
UserRate int // 单用户每秒请求数
UserCap int
}
type MultiDimensionLimiter struct {
client *redis.Client
config RateLimitConfig
limiters sync.Map // 本地缓存,减少Redis访问
}
func NewMultiDimensionLimiter(client *redis.Client, config RateLimitConfig) *MultiDimensionLimiter {
return &MultiDimensionLimiter{client: client, config: config}
}
func (m *MultiDimensionLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 全局限流
globalLimiter := NewRedisTokenBucket(m.client, m.config.GlobalRate, m.config.GlobalCap, "global")
allowed, err := globalLimiter.Allow(ctx)
if err != nil || !allowed {
respondRateLimited(w, "全局限流")
return
}
// IP限流
ip := getClientIP(r)
ipLimiter := NewRedisTokenBucket(m.client, m.config.IPRate, m.config.IPCap, "ip:"+ip)
allowed, err = ipLimiter.Allow(ctx)
if err != nil || !allowed {
respondRateLimited(w, "IP限流")
return
}
// 用户限流(已认证用户)
userID := getUserIDFromContext(ctx)
if userID != "" {
userLimiter := NewRedisTokenBucket(m.client, m.config.UserRate, m.config.UserCap, "user:"+userID)
allowed, err = userLimiter.Allow(ctx)
if err != nil || !allowed {
respondRateLimited(w, "用户限流")
return
}
}
next.ServeHTTP(w, r)
})
}
func respondRateLimited(w http.ResponseWriter, dimension string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"code":429,"dimension":"` + dimension + `","message":"请求过于频繁,请稍后重试"}`))
}
func getClientIP(r *http.Request) string {
// 从X-Forwarded-For或X-Real-IP获取
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.Split(xff, ",")[0]
}
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
return r.RemoteAddr
}
多维度限流的执行顺序应该从粗到细:先全局限流快速拦截攻击,再IP限流防止单点滥用,最后用户限流保障公平性。每个维度独立计数,互不影响。429响应中携带Retry-After头部,指导客户端退避重试。维度信息写入响应体,便于客户端日志排查限流原因。
对于Go 1.22+项目,可以直接用标准库的ServeMux配合中间件使用:
func main() {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
limiter := NewMultiDimensionLimiter(rdb, RateLimitConfig{
GlobalRate: 10000, GlobalCap: 20000,
IPRate: 100, IPCap: 200,
UserRate: 50, UserCap: 100,
})
mux := http.NewServeMux()
mux.HandleFunc("POST /api/orders", handleCreateOrder)
// 限流中间件包裹所有路由
server := &http.Server{
Addr: ":8080",
Handler: limiter.Middleware(mux),
}
server.ListenAndServe()
}
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-xian-liu-qi-shi-zhan-ling-pai-tong-yu-hua-dong/