Golang-常用限流算法实现

256 阅读1分钟

常用的限流算法有一下4中实现方式:

  • 令牌桶
  • 漏桶
  • 计数器
  • 滑动窗口

令牌桶

令牌桶以恒定的速度向桶里加入令牌,桶满了则不再加入令牌。当服务收到请求时尝试从桶中取出一个令牌,如果可以获取到令牌,则继续执行后续的业务,否则返回超限错误码或对应的错误页面。

特点:令牌桶可以支持突发流量

代码实现:

package main

import (
   "fmt"
   "sync"
   "time"
)

// TokenBucket 令牌桶
type TokenBucket struct {
   cap       int       // 容量
   rate      int       // 速率(个/秒)
   tokens    int       // 当前令牌数
   lastCheck time.Time // 上次检查时间

   lock sync.Mutex // lock 避免并发问题
}

// NewTokenBucket 构造令牌桶
func NewTokenBucket(cap, rate int) *TokenBucket {
   return &TokenBucket{
      cap:       cap,
      rate:      rate,
      tokens:    cap,
      lastCheck: time.Now(),
   }
}

// Allow 
func (tb *TokenBucket) Allow() bool {
   tb.lock.Lock()
   defer tb.lock.Unlock()
   now := time.Now()
   // 当前令牌数等于 时间间隔 * rate + 剩余的令牌数
   // 超过容量则设置为容量
   tb.tokens += int(now.Sub(tb.lastCheck).Seconds() * float64(tb.rate))
   if tb.tokens > tb.cap {
      tb.tokens = tb.cap
   }
   tb.lastCheck = now
   if tb.tokens > 0 {
      tb.tokens--
      return true
   }
   return false
}

func main() {
   tb := NewTokenBucket(10, 2)
   for i := 0; i < 20; i++ {
      if tb.Allow() {
         fmt.Println("allow", i)
      } else {
         fmt.Println("forbid")
      }
   }
}

漏桶

漏桶的容量是固定的。当有请求到来时先放到桶中,处理请求的worker以固定的速度从木桶中取出请求进行执行。如果桶已经满了,返回超限错误码或对应的错误页面

特点:流量最均匀的限流实现方式,一般用于流量“整形”

package main

import (
   "fmt"
   "sync"
   "time"
)

type LeakyBucket struct {
   cap       int       // 容量
   rate      int       // 速率(r/s)
   cur       int       // 当前容量
   lastCheck time.Time // 上次check时间

   lock sync.Mutex
}

func NewLeakyBucket(cap, rate int) *LeakyBucket {
   return &LeakyBucket{
      cap:       cap,
      rate:      rate,
      cur:       0,
      lastCheck: time.Now(),
   }
}

func (lb *LeakyBucket) Allow() bool {
   lb.lock.Lock()
   defer lb.lock.Unlock()

   now := time.Now()
   // 更新桶中当前数量
   lb.cur -= int(now.Sub(lb.lastCheck).Seconds() * float64(lb.rate))
   if lb.cur < 0 {
      lb.cur = 0
   }
   lb.lastCheck = now
   if lb.cur > lb.cap {
      return false
   }
   lb.cur++
   return true
}

func main() {
   lb := NewLeakyBucket(10, 2)
   for i := 0; i < 20; i++ {
      if lb.Allow() {
         fmt.Println("allow", i)
      } else {
         fmt.Println("forbid")
      }
   }
}

计数器

计数器限流算法指 在固定窗口内对请求进行计数,与阀值进行比较,超过阀值则返回超频错误或对应的页面。到达时间临界点,计数清零

特点:简单 问题:临界点可能会出现阀值2倍的并发请求打到服务

package main

import (
   "fmt"
   "sync"
   "time"
)

// CountLimiter 计数器
// 可以增加 cycle 参数来指定计数周期
type CountLimiter struct {
   limit     int       // 限制的最大请求数
   count     int       // 当前请求数
   lastCheck time.Time // 上次校验时间

   lock sync.Mutex
}

func NewCountLimiter(limit int) *CountLimiter {
   return &CountLimiter{
      limit:     limit,
      lastCheck: time.Now(),
   }
}

func (cl *CountLimiter) Allow() bool {
   cl.lock.Lock()
   defer cl.lock.Unlock()
   now := time.Now()

   if now.Sub(cl.lastCheck).Seconds() > 1 {
      cl.count = 0
   }
   cl.lastCheck = time.Now()
   if cl.count < cl.limit {
      cl.count++
      return true
   }
   return false
}

func main() {
   cl := NewCountLimiter(10)
   for i := 0; i < 20; i++ {
      if cl.Allow() {
         fmt.Println("allow", i)
      } else {
         fmt.Println("forbid")
      }
   }
}

滑动窗口

滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。

对于滑动时间窗口,我们可以把1s的时间窗口划分成10个小窗口(time slot), 每个time slot统计某个100ms的请求数量。每经过100ms,有一个新的time slot加入窗口,早于当前时间1s的time slot出窗口。窗口内最多维护10个time slot

package main

import (
   "fmt"
   "sync"
   "time"
)

type SlidingWindowCounter struct {
   windowSize      int           // 窗口数
   windows         []int         // 每个窗口的请求数
   interval        time.Duration // 每个窗口时间间隔
   lastCheck       time.Time     // 上次检查时间
   currentWindowId int           // 当前窗格id
   currentCount    int           // 当前请求数
   maxReq          int           // 请求阀值

   lock sync.Mutex
}

func NewSlidingWindowCounter(maxReq, windowSize int) *SlidingWindowCounter {
   return &SlidingWindowCounter{
      windowSize:      windowSize,
      windows:         make([]int, windowSize),
      interval:        time.Second / time.Duration(windowSize),
      lastCheck:       time.Now(),
      currentWindowId: 0,
      maxReq:          maxReq,
   }
}

func (swc *SlidingWindowCounter) Allow() bool {
   swc.lock.Lock()
   defer swc.lock.Unlock()
   // 新的小窗口
   if time.Since(swc.lastCheck) > swc.interval {
      swc.currentCount -= swc.windows[swc.currentWindowId]
      swc.currentWindowId++
      // 环形buff
      swc.currentWindowId = swc.currentWindowId % swc.windowSize
      swc.windows[swc.currentWindowId] = 0
      swc.lastCheck = time.Now()
   }
   swc.windows[swc.currentWindowId]++
   swc.currentCount++

   return swc.currentCount <= swc.maxReq
}

func main() {
   swc := NewSlidingWindowCounter(4, 4)
   for i := 0; i < 20; i++ {
      if swc.Allow() {
         fmt.Println("allow", swc.maxReq, swc.currentCount)
      } else {
         fmt.Println("forbid", swc.maxReq, swc.currentCount)
      }
   }
}

总结:

  • 令牌桶算法:面对大量请求时具有柔性,且请求处理速度可以随请求速度而变
  • 漏桶算法:面对大量请求时具有柔性,但请求处理速度恒定;
  • 计数器算法:存在窗口临界问题
  • 滑动窗口算法:解决了临界问题,但面对大量请求时不具有柔性;