限流策略之令牌桶限流

2023年 10月 5日 69.5k 0

一、限流模型

常见的限流模型有:

  • 令牌桶算法(Token Bucket): 令牌桶算法是一种基于令牌的限流算法。在令牌桶模型中,系统以固定速率生成令牌,并将这些令牌放入桶中。每个请求需要获取一个令牌才能执行。如果桶中没有足够的令牌,请求将被拒绝或等待。这种模型提供了平滑的限流效果。
  • 漏桶算法(Leaky Bucket): 漏桶算法是一种基于漏桶的限流算法。在漏桶模型中,请求被认为是水滴,这些水滴以固定的速率进入一个漏桶。如果漏桶已满,多余的水滴将被丢弃。漏桶模型提供了一种固定的请求处理速率。
  • 计数器算法: 计数器算法是一种简单的限流模型,它基于计数器来限制请求的频率。例如,可以设置一个计数器,每秒钟只允许处理一定数量的请求。如果请求超过了允许的数量,后续的请求将被拒绝。
  • 滑动窗口算法: 滑动窗口算法维护一个固定大小的时间窗口,内部记录请求的数量。在任意时间点,窗口内的请求数不能超过预定的限制。这种算法可以提供更灵活的控制,例如每秒的请求数限制,而不仅仅是固定速率。
  • 令牌桶 + 漏桶组合: 有时候,令牌桶和漏桶算法会结合使用,以兼顾平滑性和固定速率两方面的要求。这种组合可以提供更灵活的限流控制。
  • 基于演进窗口的限流算法: 这是一种自适应的限流算法,根据系统实际的处理情况动态调整限流策略。该算法通常结合了滑动窗口的思想,根据实际观察到的系统状况来动态调整限流参数。
  • 其他限流模型不再过多赘述,这里只聊一聊令牌桶限流。

    二、令牌桶限流原理

    令牌桶算法是一种常见的限流算法,用于控制在一个时间段内通过的请求速率。它的原理相对简单,基于一个桶(Bucket)来控制请求的流量。
    下面是令牌桶算法的基本原理:

  • 令牌桶: 算法中维护一个令牌桶,该桶以固定的速率生成令牌。桶中的每个令牌代表一个可以被处理的请求。
  • 令牌生成: 每隔一段固定的时间,令牌桶会生成一个令牌,并将其放入桶中。生成的速率决定了允许通过的请求速率。
  • 请求处理: 当一个请求到达时,系统首先检查桶中是否有可用的令牌。如果有令牌,请求被允许处理,并从桶中取走一个令牌;如果桶中没有令牌,则请求被拒绝或等待。
  • 平滑限流: 由于令牌桶以固定速率生成令牌,因此请求的处理速率是平滑的,而不是突发的。即使在某个瞬间有多个请求同时到达,只要桶中有足够的令牌,这些请求也会被平滑地处理。
  • image.png
    (图片来源于网络)

    三、使用介绍

    代码关键部分均做注释,不过多详解

    桶结构和配置:

    type UserTokenBucket struct {  
        Capacity int64 // 令牌桶的容量  
        Rate float64 // 令牌放入速率  
        Tokens float64 // 当前令牌数量  
        LastToken time.Time // 上一次放令牌的时间  
        Requests int // 当前用户的请求计数器  
        Interval time.Duration // 时间间隔  
        Lock sync.Mutex // 互斥锁  
        TimerStarted bool // 计时器是否已启动  
        Timer *time.Timer // 计时器  
    }  
      
    type LimitHandlerConfig struct {  
        MaxConn int64 // 最大连接数  
        Interval time.Duration // 时间间隔  
        MaxReq int // 最大请求次数  
    }
    

    实例桶结构和判断是否允许请求:

    func GetUserTokenBucket(key string, lock *sync.Mutex, userBuckets map[string]*UserTokenBucket, config LimitHandlerConfig) *UserTokenBucket {  
        lock.Lock()  
        defer lock.Unlock()  
      
        // 检查用户是否已有令牌桶  
        tb, found := userBuckets[key]  
        if !found {  
            // 创建新的令牌桶  
            tb = &UserTokenBucket{  
                Capacity: config.MaxConn,  
                Rate: 1.0,  
                Tokens: 0,  
                LastToken: time.Now(),  
                Requests: 0,  
                Interval: config.Interval,  
                Lock: sync.Mutex{},  
            }  
            // 将令牌桶添加到用户令牌桶映射中  
            userBuckets[key] = tb  
      
            // 启动定时器以重置用户请求计数器  
            go func() {  
                time.Sleep(config.Interval)  
                lock.Lock()  
                delete(userBuckets, key)  
                lock.Unlock()  
            }()  
        }  
        return tb  
    }  
      
    func (tb *UserTokenBucket) Allow(maxRequests int, duration time.Duration) bool {  
        tb.Lock.Lock()  
        defer tb.Lock.Unlock()  
      
        now := time.Now()  
        // 计算需要放的令牌数量  
        tb.Tokens += tb.Rate * now.Sub(tb.LastToken).Seconds()  
        // 限制令牌数量不超过容量  
        if tb.Tokens > float64(tb.Capacity) {  
            tb.Tokens = float64(tb.Capacity)  
        }  
      
        // 判断是否允许请求  
        if tb.Requests >= maxRequests && duration > time.Since(tb.LastToken) {  
            return false  
        } else if duration < time.Since(tb.LastToken) {  
            // 用户超过请求限制,重置计数器和计时器  
            tb.Requests = 0  
            tb.LastToken = now  
            if tb.TimerStarted {  
                tb.Timer.Stop()  
            }  
            tb.TimerStarted = false  
        }  
        // 更新计数器和令牌桶状态  
        tb.Requests++  
        tb.Tokens -= 1  
        tb.LastToken = now  
      
        // 启动计时器以重置计数器  
        if !tb.TimerStarted {  
            tb.Timer = time.AfterFunc(tb.Interval, func() {  
                tb.Lock.Lock()  
                tb.Requests = 0  
                tb.TimerStarted = false  
                tb.Lock.Unlock()  
            })  
            tb.TimerStarted = true  
        }  
        return true  
    }
    

    代码逻辑:

    限流中间件:

    func NewLimitHandler(config tools.LimitHandlerConfig) gin.HandlerFunc {  
        userBuckets := make(map[string]*tools.UserTokenBucket)  
        lock := sync.Mutex{}  
        return func(c *gin.Context) { 
            //获取用户标识示例:
            //具体根据自己代码修改
            ip := c.ClientIP()  
            ua := c.Request.UserAgent()  
            Key := ip + "_" + ua  
    
            // 获取或创建用户的令牌桶  
            tb := tools.GetUserTokenBucket(key, &lock, userBuckets, config)  
            // 检查用户是否超过了时间间隔内的请求限制  
            if !tb.Allow(config.MaxReq, config.Interval) {  
                c.String(503, "Too many requests")  
                c.Abort()  
                return  
            }  
            // 允许请求通过  
            c.Next()  
        }  
    }
    

    路由方法:

    //根据自己代码逻辑修改,可以将结构体实例写到配置文件中,这里不过多展开
    var limitHandlerConfig = LimitHandlerConfig{  
        MaxConn: 10,  
        Interval: time.Second * 5,  
        MaxReq: 5,  
    }
    
    r.GET("/helloWorld", logic.NewLimitHandler(limitHandlerConfig), func(c *gin.Context) {  
        c.String(200, "Hello, World!")  
    })
    

    总结

    令牌桶具有:

  • 平滑性: 令牌桶算法可以提供平滑的请求处理速率。由于令牌以固定速率生成,请求的处理速率是均匀的,不会出现瞬时的高峰。
  • 适应性: 令牌桶算法非常适应处理突发流量。即使在某个瞬时时间内出现了大量的请求,令牌桶算法也会以固定速率生成令牌,从而平滑地处理这些请求。
  • 简单可靠: 令牌桶算法的实现相对简单,容易理解和部署。它是一种可靠的算法,已在许多实际应用中得到验证。
  • 可调节性: 通过调整令牌桶的生成速率和容量,可以灵活地调节系统对请求的处理能力,适应不同的需求和场景。
  • 弹性限流: 令牌桶算法可以提供一定的弹性限流。在处理过多请求时,系统会根据令牌的数量决定是否拒绝或延迟处理请求。
  • 预测性: 由于令牌桶算法的平滑性,系统管理员可以相对容易地预测和计划系统资源的使用情况。
  • 等优点。

    总体而言,令牌桶算法是一种灵活、平滑和可控制的限流算法,适用于多种不同类型的应用场景。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论