软件系统限流的底层原理解析

2024-08-27 16:25:53 浏览数 (3)

作者:腾讯云天御业务安全工程师 knightwwang

在软件架构中,限流是一种控制资源使用和保护系统安全的重要机制。它通过限制在一定时间内可以处理的请求数量,来防止系统过载。

1. 限流的目的

限流主要有两个目的:

  • 防止系统过载:确保系统在高负载情况下仍能保持稳定运行。
  • 保证服务质量:为所有用户提供公平的服务,避免某些用户占用过多资源。

2. 限流算法的实现

2.1 固定窗口计数器算法

固定窗口计数器算法是一种基本的限流方法,它通过在固定时间窗口内跟踪请求的数量来实现限流。

代码语言:javascript复制
// 这是一个简单的实现案例
package main

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

// FixedWindowCounter 结构体实现固定窗口计数器限流算法。
// mu 用于同步访问,保证并发安全。
// count 记录当前时间窗口内的请求数量。
// limit 是时间窗口内允许的最大请求数量。
// window 记录当前时间窗口的开始时间。
// duration 是时间窗口的持续时间。
type FixedWindowCounter struct {
 mu        sync.Mutex
 count     int
 limit     int
 window    time.Time
 duration  time.Duration
}

// NewFixedWindowCounter 构造函数初始化 FixedWindowCounter 实例。
// limit 参数定义了每个时间窗口内允许的请求数量。
// duration 参数定义了时间窗口的大小。
func NewFixedWindowCounter(limit int, duration time.Duration) *FixedWindowCounter {
 return &FixedWindowCounter{
  limit:   limit,
  window:  time.Now(),   // 设置当前时间作为窗口的开始时间。
  duration: duration,    // 设置时间窗口的持续时间。
 }
}

// Allow 方法用于判断当前请求是否被允许。
// 首先通过互斥锁保证方法的原子性。
func (f *FixedWindowCounter) Allow() bool {
 f.mu.Lock()
 defer f.mu.Unlock()

 now := time.Now() // 获取当前时间。

 // 如果当前时间超过了窗口的结束时间,重置计数器和窗口开始时间。
 if now.After(f.window.Add(f.duration)) {
  f.count = 0
  f.window = now
 }

 // 如果当前计数小于限制,则增加计数并允许请求。
 if f.count < f.limit {
  f.count  
  return true
 }
 // 如果计数达到限制,则拒绝请求。
 return false
}

// main 函数是程序的入口点。
func main() {
 // 创建一个新的限流器,设置每分钟(time.Minute)只允许10个请求。
 limiter := NewFixedWindowCounter(10, time.Minute)

 // 模拟15个请求,观察限流效果。
 for i := 0; i < 15; i   {
  if limiter.Allow() {
   fmt.Println("Request", i 1, "allowed")
  } else {
   fmt.Println("Request", i 1, "rejected")
  }
 }
}

实现原理:固定窗口计数器算法通过设置一个固定的时间窗口(例如每分钟)和一个在这个窗口内允许的请求数量限制(例如10个请求)。在每个时间窗口开始时,计数器重置为零,随着请求的到来,计数器递增。当计数器达到限制时,后续的请求将被拒绝,直到窗口重置。

优点

  • 实现简单直观。
  • 容易理解和实现。
  • 可以保证在任何给定的固定时间窗口内,请求的数量不会超过设定的阈值。

缺点

  • 在窗口切换的瞬间可能会有请求高峰,因为计数器重置可能导致大量请求几乎同时被处理。
  • 无法平滑地处理突发流量,可能导致服务体验不佳。

固定窗口计数器算法适用于请求分布相对均匀的场景,但在请求可能在短时间内集中到达的场景下,可能需要考虑更复杂的限流算法,如滑动窗口或令牌桶算法。

2.2 滑动窗口算法

滑动窗口算法是固定窗口计数器算法的一个改进,它通过覆盖多个时间段来平滑请求流量,避免瞬时高峰。这种算法通常需要使用更高级的数据结构,如时间轮(Timing Wheel),来实现。

代码语言:javascript复制
// 这是一个简单的实现案例,这个代码示例仅用于说明滑动窗口限流算法的逻辑,并非完整的工作代码。

package main

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

// SlidingWindowLimiter 结构体实现滑动窗口限流算法。
type SlidingWindowLimiter struct {
 mutex       sync.Mutex
 counters    []int
 limit       int
 windowStart time.Time
 windowDuration time.Duration
 interval    time.Duration
}

// NewSlidingWindowLimiter 构造函数初始化 SlidingWindowLimiter 实例。
func NewSlidingWindowLimiter(limit int, windowDuration time.Duration, interval time.Duration) *SlidingWindowLimiter {
 buckets := int(windowDuration / interval)
 return &SlidingWindowLimiter{
  counters:    make([]int, buckets),
  limit:       limit,
  windowStart: time.Now(),
  windowDuration: windowDuration,
  interval:    interval,
 }
}

// Allow 方法用于判断当前请求是否被允许,并实现滑动窗口的逻辑。
func (s *SlidingWindowLimiter) Allow() bool {
 s.mutex.Lock()
 defer s.mutex.Unlock()

 // 检查是否需要滑动窗口
 if time.Since(s.windowStart) > s.windowDuration {
  s.slideWindow()
 }

 now := time.Now()
 index := int((now.UnixNano() - s.windowStart.UnixNano()) / s.interval.Nanoseconds()) % len(s.counters) 
 if s.counters[index] < s.limit {
  s.counters[index]  
  return true
 }
 return false
}

// slideWindow 方法实现滑动窗口逻辑,移除最旧的时间段并重置计数器。
func (s *SlidingWindowLimiter) slideWindow() {
 // 滑动窗口,忽略最旧的时间段
 copy(s.counters, s.counters[1:])
 // 重置最后一个时间段的计数器
 s.counters[len(s.counters)-1] = 0
 // 更新窗口开始时间
 s.windowStart = time.Now()
}

// main 函数是程序的入口点。
func main() {
 limiter := NewSlidingWindowLimiter(1, time.Second, 10*time.Millisecond)

 for i := 0; i < 100; i   {
  if limiter.Allow() {
   fmt.Println("Request", i 1, "allowed")
  } else {
   fmt.Println("Request", i 1, "rejected")
  }
 }
}

实现原理:滑动窗口算法通过将时间分为多个小的时间段,每个时间段内维护一个独立的计数器。当一个请求到达时,它会被分配到当前时间所在的小时间段,并检查该时间段的计数器是否已达到限制。如果未达到,则允许请求并增加计数;如果已达到,则拒绝请求。随着时间的推移,旧的时间段会淡出窗口,新的时间段会加入。

优点

  • 相比固定窗口算法,滑动窗口算法能够更平滑地处理请求,避免瞬时高峰。
  • 可以提供更细致的流量控制。

缺点

  • 实现相对复杂,需要维护多个计数器和时间索引。
  • 对内存和计算的要求更高。

滑动窗口算法适用于需要平滑流量控制的场景,尤其是在面对突发流量时,能够提供比固定窗口计数器更优的流量控制效果。

2.3 漏桶算法

漏桶算法是一种经典的流量控制方法,特别适合于平滑突发流量,确保数据以均匀的速率被处理。

代码语言:javascript复制
// 这是一个简单的实现案例,这个代码示例仅用于说明漏桶算法的基本逻辑,并非完整的工作代码。

package main

import (
    "fmt"
    "time"
)

// LeakyBucket 结构体,包含请求队列
type LeakyBucket struct {
    queue chan struct{} // 请求队列
}

// NewLeakyBucket 创建一个新的漏桶实例
func NewLeakyBucket(capacity int) *LeakyBucket {
    return &LeakyBucket{
       queue: make(chan struct{}, capacity),
    }
}

// push 将请求放入队列,如果队列满了,返回 false,表示请求被丢弃
func (lb *LeakyBucket) push() bool {
    // 如果通道可以发送,请求被接受
    select {
    case lb.queue <- struct{}{}:
       return true
    default:
       return false
    }
}

// process 从队列中取出请求并模拟处理过程
func (lb *LeakyBucket) process() {
    for range lb.queue { // 使用 range 来持续接收队列中的请求
       fmt.Println("Request processed at", time.Now().Format("2006-01-02 15:04:05"))
       time.Sleep(100 * time.Millisecond) // 模拟请求处理时间
    }
}

func main() {
    lb := NewLeakyBucket(5) // 创建一个容量为5的漏桶

    // 启动请求处理循环
    go lb.process()

    // 模拟请求
    for i := 0; i < 10; i   {
       accepted := lb.push()
       if accepted {
          fmt.Printf("Request %d accepted at %vn", i 1, time.Now().Format("2006-01-02 15:04:05"))
       } else {
          fmt.Printf("Request %d rejected at %vn", i 1, time.Now().Format("2006-01-02 15:04:05"))
       }
    }
    time.Sleep(2 * time.Second)
}

实现原理:通过一个固定容量的队列来模拟桶,以恒定速率从桶中取出请求进行处理,无论请求到达的频率如何,都保证请求以均匀的速度被处理,从而平滑流量并防止流量突增。

优点

  • 能够强制实现固定的数据处理速率,平滑流量。
  • 即使面对突发流量,也能保持稳定的处理速率。

缺点

  • 对于突发流量的处理不够灵活,可能会延迟处理。
  • 实现相对简单,但需要维护桶的状态。

漏桶算法适用于需要强制执行固定速率处理的场景,如网络流量控制、API请求限制等。通过控制令牌的添加速率,漏桶算法能够有效地避免系统因瞬时流量高峰而过载。

2.4 令牌桶算法

令牌桶算法是一种流行的限流算法,它允许一定程度的突发流量,同时保持长期的平均速率。

代码语言:javascript复制
// 这是一个简单的实现案例,这个代码示例仅用于说明令牌桶算法的基本逻辑,并非完整的工作代码。
package main

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

// TokenBucket 结构体实现令牌桶限流算法。
// - mu 用于同步访问,保证并发安全。
// - capacity 定义桶的容量,即桶中最多可以存放的令牌数。
// - tokens 表示桶中当前的令牌数。
// - refillRate 是令牌的填充速率,表示每秒向桶中添加的令牌数。
// - lastRefill 记录上次填充令牌的时间。
type TokenBucket struct {
    mu         sync.Mutex
    capacity   int
    tokens     int
    refillRate float64
    lastRefill time.Time
}

// NewTokenBucket 构造函数初始化 TokenBucket 实例。
// - capacity 参数定义了桶的容量。
// - refillRate 参数定义了每秒向桶中添加的令牌数。
func NewTokenBucket(capacity int, refillRate float64) *TokenBucket {
    // 初始化时桶被填满,tokens 和 capacity 相等。
    // lastRefill 设置为当前时间。
    return &TokenBucket{
       capacity:   capacity,
       tokens:     capacity,
       refillRate: refillRate,
       lastRefill: time.Now(),
    }
}

// Allow 方法用于判断当前请求是否被允许。
func (t *TokenBucket) Allow() bool {
    t.mu.Lock() // 进入临界区,确保操作的原子性。
    defer t.mu.Unlock()

    now := time.Now() // 获取当前时间。

    // 计算自上次填充以来经过的秒数,并转换为float64类型。
    timeElapsed := float64(now.Unix() - t.lastRefill.Unix())

    // 根据 refillRate 计算应该添加的令牌数。
    tokensToAdd := t.refillRate * timeElapsed

    // 更新令牌数,但不超过桶的容量。
    t.tokens  = int(tokensToAdd)
    if t.tokens > t.capacity {
       t.tokens = t.capacity // 确保令牌数不超过桶的容量。
    }

    // 如果桶中有令牌,则移除一个令牌并允许请求通过。
    if t.tokens > 0 {
       t.tokens--         // 移除一个令牌。
       t.lastRefill = now // 更新上次填充时间到当前时间。
       return true
    }

    // 如果桶中无令牌,则请求被拒绝。
    return false
}

// main 函数是程序的入口点。
func main() {
    // 创建一个新的令牌桶实例,桶的容量为10,每秒填充2个令牌。
    limiter := NewTokenBucket(10, 2)

    // 模拟请求,观察限流效果。
    // 循环15次,每次请求判断是否被允许。
    for i := 0; i < 15; i   {
       if limiter.Allow() {
          fmt.Println("Request", i 1, "allowed")
       } else {
          fmt.Println("Request", i 1, "rejected")
       }
    }
}

实现原理:令牌桶算法使用一个令牌桶来调节数据流的速率,允许一定程度的流量突发。桶初始时为空,并以固定的速率填充令牌,直至达到预设的容量上限。与漏桶算法不同,令牌桶算法在桶未满时,可以在每个时间间隔内向桶中添加多个令牌,从而积累处理突发请求的能力。当请求到达时,如果桶中存在令牌,算法会从桶中移除相应数量的令牌来处理请求。如果桶中的令牌不足,请求将被延迟处理或根据策略拒绝服务。如果桶已满,额外的令牌将不会被添加,确保了令牌数量不会超过桶的容量限制。

优点

  • 允许一定程度的突发流量,更加灵活。
  • 可以平滑流量,同时在桶未满时快速处理请求。

缺点

  • 实现相对复杂,需要维护桶的状态和时间。
  • 对于计算和同步的要求更高。

令牌桶算法适用于需要处理突发流量的场景,如网络通信、API调用等。通过控制令牌的填充速率和桶的容量,令牌桶算法能够有效地平衡流量,防止系统过载,同时允许在短期内处理更多的请求。

3. 限流的实现方式

限流可以通过不同的组件和层次实现

3.1 应用层限流

应用层限流是在应用程序的代码中直接实现限流逻辑,这通常是通过使用中间件来完成的。中间件可以在处理请求之前先进行限流检查,以决定是否继续处理请求或者返回错误信息。

代码语言:javascript复制
// 这是一个伪代码案例,演示实现逻辑
package main

import (
 "fmt"
 "github.com/gin-gonic/gin" // 引入Gin框架,用于构建Web服务器和处理HTTP请求
 "net/http"
 "sync"                // 引入sync包,用于同步原语,如互斥锁
 "time"                 // 引入time包,用于时间相关操作
)

// TokenBucket 结构体实现令牌桶限流算法。
// 它包含互斥锁mu用于同步访问,capacity代表桶的容量,
// tokens表示当前桶中的令牌数,refillRate是令牌的填充速率(每秒),
// lastRefill记录上次填充的时间。
type TokenBucket struct {
 mu        sync.Mutex
 capacity  int
 tokens    int
 refillRate float64
 lastRefill time.Time
}

// NewTokenBucket 函数创建并初始化一个新的TokenBucket实例。
// 它设置桶的容量和填充速率,并将初始令牌数设为容量的值。
func NewTokenBucket(capacity int, refillRate float64) *TokenBucket {
 return &TokenBucket{
  capacity:  capacity,
  tokens:    capacity,  // 初始化时桶被填满
  refillRate: refillRate,
  lastRefill: time.Now(), // 记录创建时的时间作为上次填充时间
 }
}

// Allow 方法用于检查是否允许通过当前请求。
// 它首先获取锁,然后计算自上次填充以来应该添加的令牌数,
// 更新桶中的令牌数,但不超过桶的容量。
// 如果桶中至少有一个令牌,它将减少一个令牌并返回true,表示请求被允许。
// 如果桶为空,则返回false,表示请求被拒绝。
func (tb *TokenBucket) Allow() bool {
 tb.mu.Lock() // 获取锁,保证操作的原子性
 defer tb.mu.Unlock()

 now := time.Now() // 获取当前时间
 // 计算自上次填充以来经过的秒数,然后乘以填充速率,得到应添加的令牌数
 tokensToAdd := int(tb.refillRate * (now.Sub(tb.lastRefill).Seconds()))
 tb.tokens  = tokensToAdd // 更新桶中的令牌数
 if tb.tokens > tb.capacity {
  tb.tokens = tb.capacity // 确保不超过桶的容量
 }

 if tb.tokens > 0 {
  tb.tokens-- // 处理请求,减少一个令牌
  tb.lastRefill = now // 更新上次填充时间为当前时间
  return true
 }
 return false // 如果桶为空,返回false
}

// Middleware 函数返回一个Gin中间件,该中间件使用TokenBucket来限流。
// 如果TokenBucket的Allow方法返回false,中间件将中断请求处理,
// 并返回HTTP状态码429(Too Many Requests)和错误信息。
// 如果请求被允许,中间件将调用c.Next()继续执行后续的处理链。
func Middleware(tb *TokenBucket) gin.HandlerFunc {
 return func(c *gin.Context) {
  // 在处理请求之前,调用TokenBucket的Allow方法检查是否允许请求
  if !tb.Allow() {
   // 如果请求被限流,返回错误信息和状态码
   c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
   c.Abort() // 中断请求处理
   return
  }
  // 如果请求未被限流,继续执行后续的处理链
  c.Next()
 }
}

func main() {
 // 创建一个Gin的默认实例,用于Web服务
 r := gin.Default()

 // 创建TokenBucket实例,用于限流控制
 tb := NewTokenBucket(10, 1.0) // 桶的容量为10,每秒填充1个令牌

 // 使用上面定义的限流中间件
 r.Use(Middleware(tb))

 // 定义一个简单的路由,当访问/hello路径时,返回JSON格式的消息
 r.GET("/hello", func(c *gin.Context) {
  c.JSON(http.StatusOK, gin.H{"message": "hello world"})
 })

 // 启动Gin服务器,默认监听在0.0.0.0:8080
 r.Run()
}

实现原理:在Web应用程序中,限流可以通过中间件实现。中间件在处理HTTP请求之前先执行,可以用来进行身份验证、日志记录、限流等操作。在上述代码中,创建了一个TokenBucket类型的限流器,并实现了一个Middleware函数,该函数接收一个TokenBucket实例作为参数,并返回一个Gin中间件处理器。中间件在处理请求时首先调用Allow方法检查是否允许请求通过。

优点

  • 易于实现和集成,可以轻松地添加到现有的Web应用程序中。
  • 细粒度控制,可以针对不同的路由或用户应用不同的限流策略。

缺点

  • 可能会增加请求处理的延迟,因为中间件需要在每次请求时进行同步操作。
  • 如果不恰当地使用,可能会降低应用程序的并发处理能力。

应用层限流适用于需要细粒度控制的场景,允许开发者根据具体的业务需求定制限流策略。通过合理配置限流器的参数,可以在保证服务质量的同时,提高应用程序的吞吐量和稳定性。

3.2 代理层限流

代理层限流是在网络通信的代理服务器层面实现限流,例如使用Nginx或HAProxy等代理服务器。这种方法可以在请求到达后端服务之前对它们进行限制,从而保护后端服务不受过多请求的冲击。

Nginx配置示例
代码语言:javascript复制
http {
    # 定义一个限流区域,使用共享内存存储状态
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;

    server {
        # 监听80端口
        listen 80;

        # 定义一个location块,用于匹配特定的请求路径
        location /api/ {
            # 应用限流规则
            limit_req zone=mylimit burst=5 nodelay;

            # 代理请求到后端服务
            proxy_pass http://backend/;
        }
    }
}

实现原理:在Nginx中,通过定义limit_req_zone指令创建一个限流区域,并指定使用共享内存来存储客户端IP地址和对应的请求计数。rate参数定义了每个客户端每秒钟允许的请求数量。在server块中,使用limit_req指令引用之前定义的限流区域,并设置burst参数允许一定数量的突发请求。

优点

  • 在网络层面进行限流,可以保护所有后端服务,而不需要在每个应用程序中单独实现限流逻辑。
  • 减轻了后端服务的负担,因为多余的请求在到达后端之前就被拒绝了。
  • 配置灵活,可以针对不同的请求路径和客户端设置不同的限流规则。

缺点

  • 需要代理服务器支持限流功能,可能需要额外的配置和调优。
  • 对于分布式系统,可能需要额外的机制来同步状态,确保全局的限流效果。

代理层限流适用于需要在多个服务或整个应用层面控制请求的场景。通过合理配置代理服务器的限流规则,可以在不同的层面上保护系统,提高整体的稳定性和可用性。

3.3 硬件层限流

在硬件层(如负载均衡器)实现限流,可以在请求到达应用服务器之前进行控制。

4. 限流策略

限流策略是确保应用程序能够处理预期负载并防止过载的一系列规则和措施。

阈值设置

阈值设置是限流策略的基础,它决定了系统在单位时间内能够处理的最大请求数量。

伪代码示例

代码语言:javascript复制
// RateLimiterV2 结构体增加了阈值设置功能。
type RateLimiterV2 struct {
    mu     sync.Mutex
    tokens int
    capacity  int      // 桶的容量,代表最大令牌数
    refillRate float64 // 每秒填充的令牌数
    limit     int      // 请求处理的阈值,即桶的容量
}

// NewRateLimiterV2 创建一个新的RateLimiterV2实例,并设置阈值。
func NewRateLimiterV2(capacity int, refillRate float64, limit int) *RateLimiterV2 {
    return &RateLimiterV2{
        capacity:  capacity,
        refillRate: refillRate,
        limit:     limit,
    }
}

// Allow 现在考虑了设置的阈值。
func (r *RateLimiterV2) Allow() bool {
    r.mu.Lock()
    defer r.mu.Unlock()

    // 令牌桶逻辑...
    // 如果桶中的令牌数达到或超过阈值,则拒绝请求。
    if r.tokens >= r.limit {
        return false
    }

    // 允许请求逻辑...
    return true
}
请求分类

请求分类允许对不同类型的请求应用不同的限流规则,例如,对API的不同端点设置不同的阈值。

伪代码示例

代码语言:javascript复制
// RouteLimiterMap 是一个映射,存储每个路由路径对应的限流器实例。
// 键是路由的字符串表示,值是指向RateLimiterV2类型实例的指针。
var RouteLimiterMap = map[string]*RateLimiterV2{}

// SetRateLimiterForRoute 函数为指定的路由设置一个新的限流器。
// 它接受路由的路径、桶的容量、每秒填充的令牌数和请求处理的阈值作为参数,
// 并创建一个新的RateLimiterV2实例,将其存储在RouteLimiterMap中。
func SetRateLimiterForRoute(route string, capacity int, refillRate float64, limit int) {
    // 在RouteLimiterMap中为给定的路由创建或更新限流器实例。
    RouteLimiterMap[route] = NewRateLimiterV2(capacity, refillRate, limit)
}

// MiddlewareWithRoute 函数返回一个Gin中间件处理函数。
// 该中间件基于路由名称来应用限流逻辑。
func MiddlewareWithRoute(route string) gin.HandlerFunc {
    // 返回一个Gin的处理函数,该函数内部封装了限流逻辑。
    return func(c *gin.Context) {
        // 检查RouteLimiterMap中是否存在对应路由的限流器。
        // 如果存在,调用其Allow方法来决定当前请求是否应该被允许。
        if !RouteLimiterMap[route].Allow() {
            // 如果请求被限流(不允许),返回HTTP 429状态码和错误信息。
            c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
            c.Abort() // 中断请求的进一步处理。
            return    // 退出中间件函数。
        }
        // 如果请求未被限流,调用c.Next继续执行Gin的处理链。
        c.Next()
    }
}
反馈机制

反馈机制在请求被限流时向用户提供适当的反馈,如错误消息或重试后的时间。

伪代码示例

代码语言:javascript复制
// AllowWithFeedback 提供反馈的请求允许逻辑。
func (r *RateLimiterV2) AllowWithFeedback() (bool, string) {
    r.mu.Lock()
    defer r.mu.Unlock()

    // 令牌桶逻辑...
    if r.tokens >= r.limit {
        return false, "Too many requests. Please try again later."
    }

    // 允许请求逻辑...
    r.tokens-- // 移除令牌
    return true, ""
}

// 使用反馈机制的中间件。
func MiddlewareWithFeedback() gin.HandlerFunc {
    return func(c *gin.Context) {
        allowed, message := RouteLimiterMap["/api/"].AllowWithFeedback()
        if !allowed {
            c.JSON(http.StatusTooManyRequests, gin.H{"error": message})
            c.Abort()
            return
        }
        c.Next()
    }
}

5. 限流的考虑因素

在设计和实施限流机制时,需要综合考虑多个关键因素以确保限流系统的有效性和公平性。

公平性

公平性是限流设计中的首要原则,确保所有用户和客户端能够平等地访问服务。

伪代码示例

代码语言:javascript复制
// FairLimiter 结构体实现基于用户ID或IP的公平限流。
type FairLimiter struct {
    sync.Mutex
    limits map[string]*RateLimiterV2 // 为每个用户或IP维护一个独立的限流器
}

// NewFairLimiter 创建一个新的FairLimiter实例。
func NewFairLimiter(capacity int, refillRate float64) *FairLimiter {
    return &FairLimiter{
        limits: make(map[string]*RateLimiterV2),
    }
}

// Allow 根据用户ID或IP决定是否允许请求。
func (f *FairLimiter) Allow(userID string) (bool, string) {
    f.Lock()
    defer f.Unlock()

    if _, exists := f.limits[userID]; !exists {
        // 如果用户没有限流器,则创建一个新的。
        f.limits[userID] = NewRateLimiterV2(capacity, refillRate, limit)
    }

    // 使用用户的限流器检查请求。
    return f.limits[userID].AllowWithFeedback()
}
灵活性

灵活性意味着限流策略能够适应不同的流量模式和业务需求,例如在高流量期间放宽限制。

伪代码示例

代码语言:javascript复制
// FlexibleLimiter 结构体是一个灵活的限流器,允许在运行时动态调整限流参数。
type FlexibleLimiter struct {
    sync.Mutex // 使用sync.Mutex提供互斥锁功能,确保线程安全。
    capacity  int  // 桶的容量,表示最多可以存储的令牌数。
    refillRate float64 // 令牌的填充速率,表示每秒可以新增的令牌数。
    limit      int  // 请求处理的阈值,用于确定是否限流。
}

// SetParams 方法允许动态设置FlexibleLimiter的限流参数。
// 这些参数包括桶的容量、填充速率和请求处理的阈值。
func (f *FlexibleLimiter) SetParams(capacity int, refillRate float64, limit int) {
    f.Lock() // 使用互斥锁进入临界区,防止并发访问导致的数据不一致。
    defer f.Unlock() // 离开临界区前自动释放锁。

    // 更新FlexibleLimiter的参数。
    f.capacity, f.refillRate, f.limit = capacity, refillRate, limit
}

// Allow 方法根据FlexibleLimiter当前的参数决定是否允许新的请求。
// 它首先基于当前参数创建一个新的RateLimiterV2实例,然后调用它的AllowWithFeedback方法。
func (f *FlexibleLimiter) Allow() (bool, string) {
    // 根据FlexibleLimiter当前的容量、填充速率和阈值创建一个新的RateLimiterV2实例。
    rl := NewRateLimiterV2(f.capacity, f.refillRate, f.limit)
    
    // 调用RateLimiterV2的AllowWithFeedback方法,获取是否允许请求的反馈。
    // 这个方法返回一个布尔值表示是否允许请求,和一个字符串消息提供反馈信息。
    return rl.AllowWithFeedback()
}
透明性

透明性要求限流规则和当前状态对用户可见,使用户能够了解他们被限流的原因和情况。

伪代码示例

代码语言:javascript复制
// TransparentLimiter 结构体嵌入了RateLimiterV2,提供了额外的状态信息,
// 包括当前剩余的令牌数,以增强限流机制的透明性。
type TransparentLimiter struct {
    *RateLimiterV2 // 嵌入RateLimiterV2,获得其所有功能。
    currentTokens int // 存储当前桶中剩余的令牌数。
}

// AllowWithStatus 方法允许请求并返回当前限流状态。
// 它调用内嵌RateLimiterV2的AllowWithFeedback方法来决定是否允许请求,
// 并获取反馈消息,同时返回当前剩余的令牌数。
func (t *TransparentLimiter) AllowWithStatus() (bool, string, int) {
    allowed, message := t.RateLimiterV2.AllowWithFeedback() // 调用内嵌限流器的允许逻辑。
    return allowed, message, t.currentTokens // 返回是否允许、消息和当前令牌数。
}

// MiddlewareWithTransparency 函数创建一个中间件,用于在HTTP响应中包含限流状态。
// 这个中间件包装了下一个http.Handler,并在处理请求之前检查限流状态。
func MiddlewareWithTransparency(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 创建或使用全局的transparentLimiter实例来检查限流状态。
        allowed, message, tokens := transparentLimiter.AllowWithStatus()

        // 如果请求被限流(不允许),则设置HTTP头部信息和状态码,并返回错误消息。
        if !allowed {
            w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", tokens)) // 设置剩余令牌数的头部。
            w.WriteHeader(http.StatusTooManyRequests)                             // 设置HTTP状态码为429。
            fmt.Fprintln(w, message)                                            // 写入错误消息到响应体。
            return                                                                // 中断请求处理。
        }

        // 如果请求未被限流,继续执行后续的处理链。
        next.ServeHTTP(w, r)
    })
}

1 人点赞