Go实现常见的限流算法

简介: 本文介绍了五种常见的限流算法:固定窗口、滑动窗口、漏桶算法、令牌桶和滑动日志。固定窗口简单高效,但可能产生两倍突发流量;滑动窗口可避免突发问题,但可能掐断流量;漏桶算法搭配生产者消费者模式实现平滑流量;令牌桶允许一定突发流量;滑动日志适用于多级限流场景。每种算法通过Go语言实现并附有代码解读,帮助理解其工作原理与适用场景。

固定窗口

每开启一个新的窗口,在窗口时间大小内,可以通过窗口请求上限个请求。

该算法主要是会存在临界问题,如果流量都集中在两个窗口的交界处,那么突发流量会是设置上限的两倍。

go

体验AI代码助手

代码解读

复制代码

package limiter

import (
   "sync"
   "time"
)

// FixedWindowLimiter 固定窗口限流器
type FixedWindowLimiter struct {
   limit    int           // 窗口请求上限
   window   time.Duration // 窗口时间大小
   counter  int           // 计数器
   lastTime time.Time     // 上一次请求的时间
   mutex    sync.Mutex    // 避免并发问题
}

func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter {
   return &FixedWindowLimiter{
      limit:    limit,
      window:   window,
      lastTime: time.Now(),
   }
}

func (l *FixedWindowLimiter) TryAcquire() bool {
   l.mutex.Lock()
   defer l.mutex.Unlock()
   // 获取当前时间
   now := time.Now()
   // 如果当前窗口失效,计数器清0,开启新的窗口
   if now.Sub(l.lastTime) > l.window {
      l.counter = 0
      l.lastTime = now
   }
   // 若到达窗口请求上限,请求失败
   if l.counter >= l.limit {
      return false
   }
   // 若没到窗口请求上限,计数器+1,请求成功
   l.counter++
   return true
}

滑动窗口

滑动窗口类似于固定窗口,它只是把大窗口切分成多个小窗口,每次向右移动一个小窗口,它可以避免两倍的突发流量

固定窗口可以说是滑动窗口的一种特殊情况,只要滑动窗口里面的小窗口和大窗口大小一样。

当然,窗口算法都不太平滑,当窗口满了之后,无法平滑的放行后续请求。

go

体验AI代码助手

代码解读

复制代码

package limiter

import (
   "errors"
   "sync"
   "time"
)

// SlidingWindowLimiter 滑动窗口限流器
type SlidingWindowLimiter struct {
   limit        int           // 窗口请求上限
   window       int64         // 窗口时间大小
   smallWindow  int64         // 小窗口时间大小
   smallWindows int64         // 小窗口数量
   counters     map[int64]int // 小窗口计数器
   mutex        sync.Mutex    // 避免并发问题
}

// NewSlidingWindowLimiter 创建滑动窗口限流器
func NewSlidingWindowLimiter(limit int, window, smallWindow time.Duration) (*SlidingWindowLimiter, error) {
   // 窗口时间必须能够被小窗口时间整除
   if window%smallWindow != 0 {
      return nil, errors.New("window cannot be split by integers")
   }

   return &SlidingWindowLimiter{
      limit:        limit,
      window:       int64(window),
      smallWindow:  int64(smallWindow),
      smallWindows: int64(window / smallWindow),
      counters:     make(map[int64]int),
   }, nil
}

func (l *SlidingWindowLimiter) TryAcquire() bool {
   l.mutex.Lock()
   defer l.mutex.Unlock()

   // 获取当前小窗口值
   currentSmallWindow := time.Now().UnixNano() / l.smallWindow * l.smallWindow
   // 获取起始小窗口值
   startSmallWindow := currentSmallWindow - l.smallWindow*(l.smallWindows-1)

   // 计算当前窗口的请求总数
   var count int
   for smallWindow, counter := range l.counters {
      if smallWindow < startSmallWindow {
         delete(l.counters, smallWindow)
      } else {
         count += counter
      }
   }

   // 若到达窗口请求上限,请求失败
   if count >= l.limit {
      return false
   }
   // 若没到窗口请求上限,当前小窗口计数器+1,请求成功
   l.counters[currentSmallWindow]++
   return true
}

漏桶算法

漏桶是模拟一个漏水的桶,请求相当于往桶里倒水,处理请求的速度相当于水漏出的速度。

主要用于请求处理速率较为稳定的服务,需要使用生产者消费者模式把请求放到一个队列里,让消费者以一个较为稳定的速率处理。

go

体验AI代码助手

代码解读

复制代码

package limiter

import (
   "sync"
   "time"
)

// LeakyBucketLimiter 漏桶限流器
type LeakyBucketLimiter struct {
   peakLevel       int        // 最高水位
   currentLevel    int        // 当前水位
   currentVelocity int        // 水流速度/秒
   lastTime        time.Time  // 上次放水时间
   mutex           sync.Mutex // 避免并发问题
}

func NewLeakyBucketLimiter(peakLevel, currentVelocity int) *LeakyBucketLimiter {
   return &LeakyBucketLimiter{
      peakLevel:       peakLevel,
      currentVelocity: currentVelocity,
      lastTime:        time.Now(),
   }
}

func (l *LeakyBucketLimiter) TryAcquire() bool {
   l.mutex.Lock()
   defer l.mutex.Unlock()

   // 尝试放水
   now := time.Now()
   // 距离上次放水的时间
   interval := now.Sub(l.lastTime)
   if interval >= time.Second {
      // 当前水位-距离上次放水的时间(秒)*水流速度
      l.currentLevel = maxInt(0, l.currentLevel-int(interval/time.Second)*l.currentVelocity)
      l.lastTime = now
   }

   // 若到达最高水位,请求失败
   if l.currentLevel >= l.peakLevel {
      return false
   }
   // 若没有到达最高水位,当前水位+1,请求成功
   l.currentLevel++
   return true
}

func maxInt(a, b int) int {
   if a > b {
      return a
   }
   return b
}

令牌桶

与漏桶算法的相反,令牌桶会不断地把令牌添加到桶里,而请求会从桶中获取令牌,只有拥有令牌地请求才能被接受。

因为桶中可以提前保留一些令牌,所以它允许一定地突发流量通过

go

体验AI代码助手

代码解读

复制代码

package limiter

import (
   "sync"
   "time"
)

// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
   capacity      int        // 容量
   currentTokens int        // 令牌数量
   rate          int        // 发放令牌速率/秒
   lastTime      time.Time  // 上次发放令牌时间
   mutex         sync.Mutex // 避免并发问题
}

func NewTokenBucketLimiter(capacity, rate int) *TokenBucketLimiter {
   return &TokenBucketLimiter{
      capacity: capacity,
      rate:     rate,
      lastTime: time.Now(),
   }
}

func (l *TokenBucketLimiter) TryAcquire() bool {
   l.mutex.Lock()
   defer l.mutex.Unlock()

   // 尝试发放令牌
   now := time.Now()
   // 距离上次发放令牌的时间
   interval := now.Sub(l.lastTime)
   if interval >= time.Second {
      // 当前令牌数量+距离上次发放令牌的时间(秒)*发放令牌速率
      l.currentTokens = minInt(l.capacity, l.currentTokens+int(interval/time.Second)*l.rate)
      l.lastTime = now
   }

   // 如果没有令牌,请求失败
   if l.currentTokens == 0 {
      return false
   }
   // 如果有令牌,当前令牌-1,请求成功
   l.currentTokens--
   return true
}

func minInt(a, b int) int {
   if a < b {
      return a
   }
   return b
}

滑动日志

滑动日志与滑动窗口算法类似,但是滑动日志主要用于多级限流的场景,比如短信验证码1分钟1次,1小时10次,1天20次这种业务。

算法流程与滑动窗口相同,只是它可以指定多个策略,同时在请求失败的时候,需要通知调用方是被哪个策略所拦截。

go

体验AI代码助手

代码解读

复制代码

package limiter

import (
   "errors"
   "fmt"
   "sort"
   "sync"
   "time"
)

// ViolationStrategyError 违背策略错误
type ViolationStrategyError struct {
   Limit  int           // 窗口请求上限
   Window time.Duration // 窗口时间大小
}

func (e *ViolationStrategyError) Error() string {
   return fmt.Sprintf("violation strategy that limit = %d and window = %d", e.Limit, e.Window)
}

// SlidingLogLimiterStrategy 滑动日志限流器的策略
type SlidingLogLimiterStrategy struct {
   limit        int   // 窗口请求上限
   window       int64 // 窗口时间大小
   smallWindows int64 // 小窗口数量
}

func NewSlidingLogLimiterStrategy(limit int, window time.Duration) *SlidingLogLimiterStrategy {
   return &SlidingLogLimiterStrategy{
      limit:  limit,
      window: int64(window),
   }
}

// SlidingLogLimiter 滑动日志限流器
type SlidingLogLimiter struct {
   strategies  []*SlidingLogLimiterStrategy // 滑动日志限流器策略列表
   smallWindow int64                        // 小窗口时间大小
   counters    map[int64]int                // 小窗口计数器
   mutex       sync.Mutex                   // 避免并发问题
}

func NewSlidingLogLimiter(smallWindow time.Duration, strategies ...*SlidingLogLimiterStrategy) (*SlidingLogLimiter, error) {
   // 复制策略避免被修改
   strategies = append(make([]*SlidingLogLimiterStrategy, 0, len(strategies)), strategies...)

   // 不能不设置策略
   if len(strategies) == 0 {
      return nil, errors.New("must be set strategies")
   }

   // 排序策略,窗口时间大的排前面,相同窗口上限大的排前面
   sort.Slice(strategies, func(i, j int) bool {
      a, b := strategies[i], strategies[j]
      if a.window == b.window {
         return a.limit > b.limit
      }
      return a.window > b.window
   })
   fmt.Println(strategies[0], strategies[1])

   for i, strategy := range strategies {
      // 随着窗口时间变小,窗口上限也应该变小
      if i > 0 {
         if strategy.limit >= strategies[i-1].limit {
            return nil, errors.New("the smaller window should be the smaller limit")
         }
      }
      // 窗口时间必须能够被小窗口时间整除
      if strategy.window%int64(smallWindow) != 0 {
         return nil, errors.New("window cannot be split by integers")
      }
      strategy.smallWindows = strategy.window / int64(smallWindow)
   }

   return &SlidingLogLimiter{
      strategies:  strategies,
      smallWindow: int64(smallWindow),
      counters:    make(map[int64]int),
   }, nil
}

func (l *SlidingLogLimiter) TryAcquire() error {
   l.mutex.Lock()
   defer l.mutex.Unlock()

   // 获取当前小窗口值
   currentSmallWindow := time.Now().UnixNano() / l.smallWindow * l.smallWindow
   // 获取每个策略的起始小窗口值
   startSmallWindows := make([]int64, len(l.strategies))
   for i, strategy := range l.strategies {
      startSmallWindows[i] = currentSmallWindow - l.smallWindow*(strategy.smallWindows-1)
   }

   // 计算每个策略当前窗口的请求总数
   counts := make([]int, len(l.strategies))
   for smallWindow, counter := range l.counters {
      if smallWindow < startSmallWindows[0] {
         delete(l.counters, smallWindow)
         continue
      }
      for i := range l.strategies {
         if smallWindow >= startSmallWindows[i] {
            counts[i] += counter
         }
      }
   }

   // 若到达对应策略窗口请求上限,请求失败,返回违背的策略
   for i, strategy := range l.strategies {
      if counts[i] >= strategy.limit {
         return &ViolationStrategyError{
            Limit:  strategy.limit,
            Window: time.Duration(strategy.window),
         }
      }
   }

   // 若没到窗口请求上限,当前小窗口计数器+1,请求成功
   l.counters[currentSmallWindow]++
   return nil
}

总结

  • 如果需要一个简单高效的算法,可以使用固定窗口,但是它可能产生两倍的突发流量
  • 可以通过滑动窗口避免突发流量问题,但是窗口可能会掐断流量一段时间
  • 如果需要更平滑的流量,可以使用漏桶算法搭配生产者消费者模式
  • 如果能够处理一定的突发流量,可以使用令牌桶算法
  • 遇到多级限流的场景,滑动日志会更加适合


转载来源:https://juejin.cn/post/7056068978862456846

相关文章
|
4月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
6月前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
138 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
21天前
|
机器学习/深度学习 存储 监控
上网管理监控软件的 Go 语言流量特征识别算法实现与优化
本文探讨基于Go语言的流量特征识别算法,用于上网管理监控软件。核心内容涵盖AC自动机算法原理、实现及优化,通过路径压缩、哈希表存储和节点合并策略提升性能。实验表明,优化后算法内存占用降低30%,匹配速度提升20%。在1000Mbps流量下,CPU利用率低于10%,内存占用约50MB,检测准确率达99.8%。未来可进一步优化高速网络处理能力和融合机器学习技术。
63 10
|
2月前
|
存储 监控 算法
员工行为监控软件中的 Go 语言哈希表算法:理论、实现与分析
当代企业管理体系中,员工行为监控软件已逐步成为维护企业信息安全、提升工作效能的关键工具。这类软件能够实时记录员工操作行为,为企业管理者提供数据驱动的决策依据。其核心支撑技术在于数据结构与算法的精妙运用。本文聚焦于 Go 语言中的哈希表算法,深入探究其在员工行为监控软件中的应用逻辑与实现机制。
82 14
|
2月前
|
存储 算法 物联网
解析局域网内控制电脑机制:基于 Go 语言链表算法的隐秘通信技术探究
数字化办公与物联网蓬勃发展的时代背景下,局域网内计算机控制已成为提升工作效率、达成设备协同管理的重要途径。无论是企业远程办公时的设备统一调度,还是智能家居系统中多设备间的联动控制,高效的数据传输与管理机制均构成实现局域网内计算机控制功能的核心要素。本文将深入探究 Go 语言中的链表数据结构,剖析其在局域网内计算机控制过程中,如何达成数据的有序存储与高效传输,并通过完整的 Go 语言代码示例展示其应用流程。
54 0
|
4月前
|
算法 安全 Go
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
本文探讨了如何利用 Go 语言中的 Bloom Filter 算法提升公司局域网管理系统的性能。Bloom Filter 是一种高效的空间节省型数据结构,适用于快速判断元素是否存在于集合中。文中通过具体代码示例展示了如何在 Go 中实现 Bloom Filter,并应用于局域网的 IP 访问控制,显著提高系统响应速度和安全性。随着网络规模扩大和技术进步,持续优化算法和结合其他安全技术将是企业维持网络竞争力的关键。
94 2
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
|
4月前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
64 3
|
4月前
|
存储 监控 算法
探秘员工泄密行为防线:基于Go语言的布隆过滤器算法解析
在信息爆炸时代,员工泄密行为对企业构成重大威胁。本文聚焦布隆过滤器(Bloom Filter)这一高效数据结构,结合Go语言实现算法,帮助企业识别和预防泄密风险。通过构建正常操作“指纹库”,实时监测员工操作,快速筛查可疑行为。示例代码展示了如何利用布隆过滤器检测异常操作,并提出优化建议,如调整参数、结合日志分析系统等,全方位筑牢企业信息安全防线,守护核心竞争力。
|
4月前
|
存储 算法 安全
基于 Go 语言的公司内网管理软件哈希表算法深度解析与研究
在数字化办公中,公司内网管理软件通过哈希表算法保障信息安全与高效管理。哈希表基于键值对存储和查找,如用户登录验证、设备信息管理和文件权限控制等场景,Go语言实现的哈希表能快速验证用户信息,提升管理效率,确保网络稳定运行。
64 0
|
5月前
|
存储 监控 算法
内网监控系统之 Go 语言布隆过滤器算法深度剖析
在数字化时代,内网监控系统对企业和组织的信息安全至关重要。布隆过滤器(Bloom Filter)作为一种高效的数据结构,能够快速判断元素是否存在于集合中,适用于内网监控中的恶意IP和违规域名筛选。本文介绍其原理、优势及Go语言实现,提升系统性能与响应速度,保障信息安全。
66 5
下一篇
oss创建bucket