限流策略之令牌桶限流

一、限流模型

常见的限流模型有:

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

其他限流模型不再过多赘述,这里只聊一聊令牌桶限流。

二、令牌桶限流原理

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

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

(图片来源于网络)

三、使用介绍

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

桶结构和配置:

go 复制代码
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 // 最大请求次数  
}

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

go 复制代码
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  
}

代码逻辑:

限流中间件:

go 复制代码
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()  
    }  
}

路由方法:

go 复制代码
//根据自己代码逻辑修改,可以将结构体实例写到配置文件中,这里不过多展开
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!")  
})

总结

令牌桶具有:

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

等优点。

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

相关推荐
程序员大金36 分钟前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
程序员大金1 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
Pandaconda1 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Ylucius2 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
ღ᭄ꦿ࿐Never say never꧂3 小时前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴3 小时前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
海里真的有鱼3 小时前
Spring Boot 中整合 Kafka
后端