goWe中的接口限流理论与实战
背景
刚入职没有几天小编接到了一个接口进行限流的需求,限流的本质就是从时间维度判断请求是否满足要求,不满足将不会执行业务逻辑,从而减少服务处理业务逻辑的承载量。对接口进行限流一方面就是防止服务器承载过多的流量,另外就是限制别人频繁的爬虫。由于之前做java的时候也看过相关的面经了解过限流算法,但是没有真正的实战过,本文先介绍限流算法以及实现(主要是桶令牌算法)。下面列举一些限流的维度,可能会有更多的限流维度。
- 服务维度
- 用户维度
- ip地址维度
固定窗口算法
起初小编和大多数人一样想到的就是,取一个时间段计数请求量,如果大于限制的数量就返回直接限制执行业务逻辑。但是这样会出现限流时间的不准确,如果人为统计两个计数之间的时间段就可能不是限制的次数,最多会允许两个时间段的次数-1。这样流量几种请求后,剩余时间间隔内的请求所有将都会限流,这也是这种算法的一大缺点。
实现方案(对用户进行限流):使用redis可以设置key-value的失效时间,第一次访问的时候设置userId-1并加上存活时间,下次访问的时候,查询key的值是否存在(不存在和第一次访问操作一致)判断请求次数是否满足,如果满足对值进行+1操作,不满足直接返回错误结果。
滑动窗口思想下的算法
既然固定时间窗口算法会出现时间段之间的漏记,那我们就把时间窗口进行滑动。首先将固定窗口进行更小的时间间隔划分,作为每次窗口的滑动长度。
漏斗算法
漏斗算法主要是根据计算请求的速率来计算请求是否满足限流要求。
桶令牌算法
主要流程就是只有从桶中获取得令牌的请求才能通过,同时不断往桶里加令牌,但是桶的容积是一定的。
基于时间差的循环计数法实现
利用上一次和本次时间差来判断新增桶的令牌。
type TokenBucket struct {
capacity int // 桶的容量
fillRate int // 每秒的填充速率
tokens int // 当前的令牌数
last float64 // 上次增加令牌的时间
}
//通过时间差判断这段时间桶中令牌的增加数量
func (b *TokenBucket) AddTokens() {
now := time.Now().UnixNano() / int64(time.Millisecond)
elapsed := now - b.last
b.tokens += elapsed * b.fillRate / 1000
//增加数量大于桶的容量认为桶中令牌数量为桶的容量
if b.tokens > b.capacity {
b.tokens = b.capacity
}
b.last = now
}
func (b *TokenBucket) ConsumeToken() bool {
b.AddTokens()
//消费令牌
if b.tokens > 0 {
b.tokens--
return true
}
return false
}
//注意调用ConsumeToken的时候要加锁,上面只实现了对服务进行限流,如果要对用户限流,需要创建多个桶保存起来
基于redis实现限流实现
这种方式相当于把redis中抽象出一个桶,相关key的数量不能大于桶容量,利用redis的key时间到期会被删除的特性,相当于相关key到期删除后就是定时增加了一个令牌,为了提高扩展性,我将限流写成了一个中间件,参数是对应key的存活时间和最大的数量在配置文件里的名字。
//单个用户在redis中存储的key的前缀相同都是limit-userId+时间戳
//统计redis相关用户key的数量,并判断是否大于等于桶的容量,是,就结束请求,否,新增key执行请求逻辑
func RateLimiter(existTimeName, maxSizeName string) gin.HandlerFunc {
var mutex sync.Mutex
return func(c *gin.Context) {
existTime := viper.GetInt(existTimeName)
maxSize := viper.GetInt(maxSizeName)
userId := c.GetUint("userId")
if userId == 0 {
ginc.ResCustomError(c, ERR.NotLogin.Msg, ERR.NotLogin.Code)
c.Abort()
return
}
key := strings.Join([]string{limiterPrefix, c.FullPath(), strconv.Itoa(int(userId)), strconv.FormatInt(time.Now().UnixMicro(), 10)}, "—")
likeKey := strings.Join([]string{limiterPrefix, c.FullPath(), strconv.Itoa(int(userId)), "*"}, "—")
mutex.Lock()
//统计key的数量
valueList := initialize.Redis.Keys(ctx, likeKey).Val()
num := len(valueList)
if num >= maxSize {
ginc.ResCommonError(c, "您的访问过于频繁,请稍后再试")
c.Abort()
return
}
err := initialize.Redis.SetNX(ctx, key, 1, time.Duration(existTime)*time.Second).Err()
if err != nil {
ginc.ResCommonError(c, "系统错误")
ginc.Log(c).Error(err)
c.Abort()
return
}
mutex.Unlock()
c.Next()
}
}