Go初级之http限流令牌桶的基本实现

2024-04-25 20:06:41 浏览数 (2)

前言

本文是记录的是" Go初级之http限流令牌桶的基本实现 "

此文是个人学习归纳的记录,腾讯云独家发布,未经允许,严禁转载,如有不对, 还望斧正, 感谢!

关于令牌桶

令牌桶是一种常用的流量控制技术,其本身没有丢弃和优先级策略。令牌桶的工作原理如下:

1. 令牌以一定的速率放入桶中。 2. 每个令牌允许源发送一定数量的比特。 3. 发送一个包,流量调节器就要从桶中删除与包大小相等的令牌数。 4. 如果没有足够的令牌发送包,这个包就会等待直到有足够的令牌(在整形器的情况下)或者包被丢弃,也有可能被标记更低的DSCP(在策略者的情况下)。 5. 桶有特定的容量,如果桶已经满了,新加入的令牌就会被丢弃。因此,在任何时候,源发送到网络上的最大突发数据量与桶的大小成比例。令牌桶允许突发,但是不能超过限制。

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

常见的限流算法

除了令牌桶,还有其他几种常用的限流算法,包括滑动窗口、计数器和漏桶。

  1. 滑动窗口:滑动窗口算法通过将时间分为固定大小的窗口来限流。每个窗口内的请求数量被计数,当请求数量超过限制时,请求将被拒绝。当窗口滑动到下一个时间段时,计数器被重置,新的请求计数开始。滑动窗口算法可以处理突发流量,但可能导致请求在窗口之间的边界处被拒绝。
  2. 计数器:计数器算法通过在固定的时间间隔内计算请求数量来限流。例如,每秒钟计算请求数量,并在达到限制时拒绝请求。计数器算法简单易实现,但可能导致突发流量问题。
  3. 漏桶:漏桶算法通过将请求放入一个有限容量的桶中来限流。请求以固定的速率进入桶,而桶以固定的速率消耗请求。当桶满时,新的请求将被丢弃。漏桶算法可以处理突发流量,并在限制请求速率的同时,允许短时间的突发流量通过。

这些限流算法各有优缺点,可以根据具体场景选择合适的算法。例如,如果需要精确控制请求速率,可以使用令牌桶或漏桶算法;如果需要简单的速率限制,可以使用计数器或滑动窗口算法。

简单地用go语言的代码实现一个限流的令牌桶

上面我已经解释很清楚了,我们通过控制令牌桶中的令牌的使用和生成来对http请求之类的流量进行控制,所以我们主要关心的就是桶的容积,桶中令牌的数量。

然后我们简单定义一个桶

代码语言:go复制
type Bucket struct {
	cap      int // 容积
	value    int // 当前令牌数
	lastTime int // 上次添加的时间
	lock     sync.Mutex  // 来个互斥锁,保证原子性
}

上次添加的时间是什么意思呢,就是我们需要动态控制桶中的令牌的数量,可以通过这个字段和当前时间进行比较,如果时间差距很大,说明请求很稀疏,然后我们可以通过自定义的方法,动态控制令牌的数量。

定义好了上面的Bucket桶结构之后,我们再写入以下代码,

代码语言:go复制
// 创建一个桶,传入桶的容积和当前的令牌数
func NewBucket(cap int, value int) *Bucket {
	return &Bucket{cap: cap, value: value, lastTime: int(time.Now().Unix())}

}

// 定义一个add函数,用来动态控制令牌的数量,比较简陋
func (B *Bucket) add() {
	now := int(time.Now().Unix())  // 得到当前的时间
	lastTime := B.lastTime            // 得到上一次请求的时间
	B.lastTime = now        
	// 当前令牌数大于容量时,不添加令牌
	if B.value >= l.cap {
		return
	}
	l.value = (now - lastTime) / 1000  // 此处就简单用时间来控制了。
	if B.value > l.cap {
		l.value = l.cap    
	}
}

// 判断当前令牌桶的令牌还有没有,返回布尔值
func (B *Bucket) IsOk() bool {
	B.lock.Lock()
	defer B.lock.Unlock()
	B.add()   // 调用令牌控制函数动态控制令牌数量
	if B.value > 0 {
		l.value--
		return true
	}
	return false
}

上面的是一个简陋的,只实现了基本原理。

下面我们写一个gin的中间件,基于第三方包的实现

先要安装 "github.com/juju/ratelimit" 这个包

代码语言:go复制
package middleware

import (
	"github.com/gin-gonic/gin"
	"github.com/juju/ratelimit"
	"strings"
	"time"
)

// SpeedLimit 限速路由结构体, 用来指定要限速的路由
type SpeedLimit struct {
	Limit map[string]*ratelimit.Bucket // 键值对,前面放路由,后面是令牌桶
}

// LimiterBucketRule 是一个结构体,用于定义令牌桶规则
type LimiterBucketRule struct {
	// Key 是键也是路由
	Key string
	// FillInterval 是令牌桶的填充间隔
	FillInterval time.Duration
	// Capacity 是令牌桶的容量
	Capacity int64
	// Quantum 是令牌桶的单次请求所需的令牌数量
	Quantum int64
}

// NewSpeedLimit 新建限速结构体
func NewSpeedLimit() *SpeedLimit {
	return &SpeedLimit{
		Limit: make(map[string]*ratelimit.Bucket),
	}
}

// NewLimiterBucketRule 新建限速规则
func NewLimiterBucketRule(key string, fillInterval time.Duration, capacity int64, quantum int64) LimiterBucketRule {
	return LimiterBucketRule{
		Key:          key,
		FillInterval: fillInterval,
		Capacity:     capacity,
		Quantum:      quantum,
	}
}

// GetKey 获取限速的键,也就是限速的路由
func (sl *SpeedLimit) GetKey(c *gin.Context) string {
	url := c.Request.URL.Path

	// 切分url
	index := strings.Index(url, "?")
	if index != -1 {
		url = url[:index]
	}
	return url
}

// GetBucket 获取限速的令牌桶
func (sl *SpeedLimit) GetBucket(key string) (*ratelimit.Bucket, bool) {
	bucket, flag := sl.Limit[key]
	return bucket, flag
}

// AddToLimit 添加限速规则
func (sl *SpeedLimit) AddToLimit(rules ...LimiterBucketRule) *SpeedLimit {
	for _, rule := range rules {
		// 检查令牌桶是否已经存在
		if _, flag := sl.Limit[rule.Key]; flag {
			sl.Limit[rule.Key] = ratelimit.NewBucketWithQuantum(rule.FillInterval, rule.Capacity, rule.Quantum)
		}
	}
	return sl
}

// SpeedLimitMiddleware gin路由限速路由中间件
func SpeedLimitMiddleware(sl *SpeedLimit) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取限速的键,也就是请求的路由
		key := sl.GetKey(c)
		// 获取该条路由限速的令牌桶
		bucket, flag := sl.GetBucket(key)
		// 判断是否要限速
		if flag {
			// 获取令牌数量
			count := bucket.TakeAvailable(1)
			if count == 0 {
				c.AbortWithStatusJSON(403, gin.H{
					"code": 403,
					"msg":  "访问过于频繁",
				})
				return
			}
		}
		c.Next() // 放行
	}
}

到这里,我们也就差不多了,前面实现的简陋的和这个实际上也差不多,有区别的地方,就是在于判断是否限速,这个需要多方考虑。

简陋的里面只是简单的用一定时间的请求量来判断了,还可以优化。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞