一、限流模型
常见的限流模型有:
- 令牌桶算法(Token Bucket): 令牌桶算法是一种基于令牌的限流算法。在令牌桶模型中,系统以固定速率生成令牌,并将这些令牌放入桶中。每个请求需要获取一个令牌才能执行。如果桶中没有足够的令牌,请求将被拒绝或等待。这种模型提供了平滑的限流效果。
- 漏桶算法(Leaky Bucket): 漏桶算法是一种基于漏桶的限流算法。在漏桶模型中,请求被认为是水滴,这些水滴以固定的速率进入一个漏桶。如果漏桶已满,多余的水滴将被丢弃。漏桶模型提供了一种固定的请求处理速率。
- 计数器算法: 计数器算法是一种简单的限流模型,它基于计数器来限制请求的频率。例如,可以设置一个计数器,每秒钟只允许处理一定数量的请求。如果请求超过了允许的数量,后续的请求将被拒绝。
- 滑动窗口算法: 滑动窗口算法维护一个固定大小的时间窗口,内部记录请求的数量。在任意时间点,窗口内的请求数不能超过预定的限制。这种算法可以提供更灵活的控制,例如每秒的请求数限制,而不仅仅是固定速率。
- 令牌桶 + 漏桶组合: 有时候,令牌桶和漏桶算法会结合使用,以兼顾平滑性和固定速率两方面的要求。这种组合可以提供更灵活的限流控制。
- 基于演进窗口的限流算法: 这是一种自适应的限流算法,根据系统实际的处理情况动态调整限流策略。该算法通常结合了滑动窗口的思想,根据实际观察到的系统状况来动态调整限流参数。
其他限流模型不再过多赘述,这里只聊一聊令牌桶限流。
二、令牌桶限流原理
令牌桶算法是一种常见的限流算法,用于控制在一个时间段内通过的请求速率。它的原理相对简单,基于一个桶(Bucket)来控制请求的流量。 下面是令牌桶算法的基本原理:
- 令牌桶: 算法中维护一个令牌桶,该桶以固定的速率生成令牌。桶中的每个令牌代表一个可以被处理的请求。
- 令牌生成: 每隔一段固定的时间,令牌桶会生成一个令牌,并将其放入桶中。生成的速率决定了允许通过的请求速率。
- 请求处理: 当一个请求到达时,系统首先检查桶中是否有可用的令牌。如果有令牌,请求被允许处理,并从桶中取走一个令牌;如果桶中没有令牌,则请求被拒绝或等待。
- 平滑限流: 由于令牌桶以固定速率生成令牌,因此请求的处理速率是平滑的,而不是突发的。即使在某个瞬间有多个请求同时到达,只要桶中有足够的令牌,这些请求也会被平滑地处理。
(图片来源于网络)
三、使用介绍
代码关键部分均做注释,不过多详解
桶结构和配置:
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!")
})
总结
令牌桶具有:
- 平滑性: 令牌桶算法可以提供平滑的请求处理速率。由于令牌以固定速率生成,请求的处理速率是均匀的,不会出现瞬时的高峰。
- 适应性: 令牌桶算法非常适应处理突发流量。即使在某个瞬时时间内出现了大量的请求,令牌桶算法也会以固定速率生成令牌,从而平滑地处理这些请求。
- 简单可靠: 令牌桶算法的实现相对简单,容易理解和部署。它是一种可靠的算法,已在许多实际应用中得到验证。
- 可调节性: 通过调整令牌桶的生成速率和容量,可以灵活地调节系统对请求的处理能力,适应不同的需求和场景。
- 弹性限流: 令牌桶算法可以提供一定的弹性限流。在处理过多请求时,系统会根据令牌的数量决定是否拒绝或延迟处理请求。
- 预测性: 由于令牌桶算法的平滑性,系统管理员可以相对容易地预测和计划系统资源的使用情况。
等优点。
总体而言,令牌桶算法是一种灵活、平滑和可控制的限流算法,适用于多种不同类型的应用场景。