Redis Rate Limiter 使用指南

简介

github.com/go-redis/redis_rate/v10 是一个基于 Redis 的分布式限流库,专为 Go 语言设计,可以轻松实现 API 限流、请求节流等功能。它基于令牌桶算法,支持多种限流策略,适用于分布式系统中的流量控制。

安装

go get github.com/go-redis/redis_rate/v10

基本用法

初始化限流器

import (
    "context"
    "github.com/redis/go-redis/v10"
    "github.com/go-redis/redis_rate/v10"
)

// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

// 创建限流器
limiter := redis_rate.NewLimiter(rdb)

简单限流示例

// 限制每分钟最多允许 10 次请求
res, err := limiter.Allow(context.Background(), "user:123", redis_rate.PerMinute(10))
if err != nil {
    // 处理错误
}

if res.Allowed > 0 {
    // 允许请求
    fmt.Println("请求被允许")
} else {
    // 拒绝请求
    fmt.Printf("请求被拒绝,请在 %v 后重试\n", res.RetryAfter)
}

限流策略

预定义的限流策略

// 每秒限制
redis_rate.PerSecond(10)  // 每秒最多 10 个请求

// 每分钟限制
redis_rate.PerMinute(100)  // 每分钟最多 100 个请求

// 每小时限制
redis_rate.PerHour(1000)  // 每小时最多 1000 个请求

自定义限流策略

// 创建自定义限流策略
limit := redis_rate.Limit{
    Rate:   20,              // 每个时间单位允许的请求数
    Burst:  5,               // 突发请求数(令牌桶容量)
    Period: time.Minute * 5, // 时间单位
}

res, err := limiter.Allow(ctx, "custom:limit", limit)

高级用法

批量限流检查

// 批量检查多个限流键
keys := []string{"user:1", "user:2", "user:3"}
limits := make([]redis_rate.Limit, len(keys))
for i := range limits {
    limits[i] = redis_rate.PerMinute(10)
}

results, err := limiter.AllowN(ctx, keys, limits, 1)
if err != nil {
    // 处理错误
}

for i, res := range results {
    fmt.Printf("Key: %s, Allowed: %d, Remaining: %d\n", 
        keys[i], res.Allowed, res.Remaining)
}

消耗多个令牌

// 一次性消耗 5 个令牌
res, err := limiter.AllowN(ctx, "heavy:operation", redis_rate.PerMinute(20), 5)

在 Web 框架中使用

Gin 框架集成示例

func RateLimitMiddleware(limiter *redis_rate.Limiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := "rate_limit:" + c.ClientIP()
        
        res, err := limiter.Allow(c.Request.Context(), key, redis_rate.PerMinute(60))
        if err != nil {
            c.AbortWithStatusJSON(500, gin.H{"error": "限流服务异常"})
            return
        }
        
        if res.Allowed == 0 {
            c.AbortWithStatusJSON(429, gin.H{
                "error": "请求过于频繁",
                "retry_after": res.RetryAfter.Seconds(),
            })
            return
        }
        
        // 设置剩余请求数的响应头
        c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", res.Remaining))
        c.Header("X-RateLimit-Limit", "60")
        
        c.Next()
    }
}

返回值解析

Allow 方法返回的 redis_rate.Result 结构包含以下字段:

type Result struct {
    // Allowed 表示允许的请求数量(0 或 1)
    Allowed int

    // Remaining 表示剩余的令牌数量
    Remaining int

    // RetryAfter 表示需要等待多久才能获取下一个令牌
    RetryAfter time.Duration

    // ResetAfter 表示多久后令牌桶会重置
    ResetAfter time.Duration
}

分布式系统中的应用

多服务实例共享限流状态

// 所有服务实例使用相同的 Redis 服务器
// 确保限流状态在所有实例间共享
rdb := redis.NewClient(&redis.Options{
    Addr: "redis-server:6379",
})
limiter := redis_rate.NewLimiter(rdb)

// 使用统一的键前缀
keyPrefix := "api:rate:"
key := keyPrefix + userID

res, err := limiter.Allow(ctx, key, redis_rate.PerMinute(100))

按不同维度限流

// 按用户 ID 限流
userKey := "user:" + userID
userRes, _ := limiter.Allow(ctx, userKey, redis_rate.PerMinute(60))

// 按 IP 限流
ipKey := "ip:" + clientIP
ipRes, _ := limiter.Allow(ctx, ipKey, redis_rate.PerMinute(100))

// 按 API 路径限流
pathKey := "path:" + requestPath
pathRes, _ := limiter.Allow(ctx, pathKey, redis_rate.PerSecond(10))

// 综合判断
if userRes.Allowed > 0 && ipRes.Allowed > 0 && pathRes.Allowed > 0 {
    // 允许请求
}

最佳实践

  1. 合理设置限流参数:根据业务需求和系统承载能力设置合适的限流阈值
  2. 使用有意义的键名:为限流键使用有意义的前缀和命名规则
  3. 监控限流情况:记录被限流的请求,以便分析和调整限流策略
  4. 优雅处理限流:当请求被限流时,返回友好的错误信息和重试建议
  5. 设置合理的超时:为 Redis 操作设置合理的超时时间,避免因 Redis 故障影响整体服务

示例:完整的限流中间件

package middleware

import (
    "context"
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/redis/go-redis/v10"
    "github.com/go-redis/redis_rate/v10"
)

// RateLimiterConfig 限流器配置
type RateLimiterConfig struct {
    // 每分钟允许的请求数
    RequestsPerMinute int
    
    // 是否启用 IP 限流
    EnableIPLimit bool
    
    // 是否启用路径限流
    EnablePathLimit bool
}

// NewRateLimiter 创建限流中间件
func NewRateLimiter(rdb *redis.Client, config RateLimiterConfig) gin.HandlerFunc {
    limiter := redis_rate.NewLimiter(rdb)
    
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*3)
        defer cancel()
        
        // 默认限流键(用户标识或会话ID)
        key := "rate:default:" + c.GetString("user_id")
        if key == "rate:default:" {
            key = "rate:session:" + c.GetString("session_id")
        }
        
        // IP 限流
        if config.EnableIPLimit {
            ipKey := "rate:ip:" + c.ClientIP()
            ipRes, err := limiter.Allow(ctx, ipKey, redis_rate.PerMinute(config.RequestsPerMinute*2))
            if err != nil || ipRes.Allowed == 0 {
                c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                    "error": "IP 请求频率超限",
                    "retry_after": ipRes.RetryAfter.Seconds(),
                })
                return
            }
        }
        
        // 路径限流
        if config.EnablePathLimit {
            pathKey := "rate:path:" + c.FullPath()
            pathRes, err := limiter.Allow(ctx, pathKey, redis_rate.PerMinute(config.RequestsPerMinute*5))
            if err != nil || pathRes.Allowed == 0 {
                c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                    "error": "接口请求频率超限",
                    "retry_after": pathRes.RetryAfter.Seconds(),
                })
                return
            }
        }
        
        // 用户/会话限流
        res, err := limiter.Allow(ctx, key, redis_rate.PerMinute(config.RequestsPerMinute))
        if err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "error": "限流服务异常",
            })
            return
        }
        
        if res.Allowed == 0 {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error": "请求过于频繁,请稍后再试",
                "retry_after": res.RetryAfter.Seconds(),
            })
            return
        }
        
        // 设置响应头
        c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", config.RequestsPerMinute))
        c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", res.Remaining))
        c.Header("X-RateLimit-Reset", fmt.Sprintf("%.0f", res.ResetAfter.Seconds()))
        
        c.Next()
    }
}

参考资料

每日更新-免费小火箭账号
不要错过任何机会,探索最新的应用和游戏,就在我们的平台。
立即访问
最后修改:2025 年 03 月 28 日
如果觉得我的文章对你有用,请随意赞赏