goWe中的接口限流理论与实战

224 阅读4分钟

goWe中的接口限流理论与实战

背景

刚入职没有几天小编接到了一个接口进行限流的需求,限流的本质就是从时间维度判断请求是否满足要求,不满足将不会执行业务逻辑,从而减少服务处理业务逻辑的承载量。对接口进行限流一方面就是防止服务器承载过多的流量,另外就是限制别人频繁的爬虫。由于之前做java的时候也看过相关的面经了解过限流算法,但是没有真正的实战过,本文先介绍限流算法以及实现(主要是桶令牌算法)。下面列举一些限流的维度,可能会有更多的限流维度。

  • 服务维度
  • 用户维度
  • ip地址维度

固定窗口算法

起初小编和大多数人一样想到的就是,取一个时间段计数请求量,如果大于限制的数量就返回直接限制执行业务逻辑。但是这样会出现限流时间的不准确,如果人为统计两个计数之间的时间段就可能不是限制的次数,最多会允许两个时间段的次数-1。这样流量几种请求后,剩余时间间隔内的请求所有将都会限流,这也是这种算法的一大缺点。

image.png

实现方案(对用户进行限流):使用redis可以设置key-value的失效时间,第一次访问的时候设置userId-1并加上存活时间,下次访问的时候,查询key的值是否存在(不存在和第一次访问操作一致)判断请求次数是否满足,如果满足对值进行+1操作,不满足直接返回错误结果。

滑动窗口思想下的算法

既然固定时间窗口算法会出现时间段之间的漏记,那我们就把时间窗口进行滑动。首先将固定窗口进行更小的时间间隔划分,作为每次窗口的滑动长度。

image.png

漏斗算法

漏斗算法主要是根据计算请求的速率来计算请求是否满足限流要求。

image.png

桶令牌算法

主要流程就是只有从桶中获取得令牌的请求才能通过,同时不断往桶里加令牌,但是桶的容积是一定的。

image.png

基于时间差的循环计数法实现

利用上一次和本次时间差来判断新增桶的令牌。

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()
    }
}