Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >软件系统限流的底层原理解析

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

作者头像
腾讯技术工程官方号
发布于 2024-08-27 08:25:53
发布于 2024-08-27 08:25:53
57901
代码可运行
举报
运行总次数:1
代码可运行
作者:腾讯云天御业务安全工程师 knightwwang

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

1. 限流的目的

限流主要有两个目的:

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

2. 限流算法的实现

2.1 固定窗口计数器算法

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这是一个简单的实现案例
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
代码运行次数:0
运行
AI代码解释
复制
// 这是一个简单的实现案例,这个代码示例仅用于说明滑动窗口限流算法的逻辑,并非完整的工作代码。

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
代码运行次数:0
运行
AI代码解释
复制
// 这是一个简单的实现案例,这个代码示例仅用于说明漏桶算法的基本逻辑,并非完整的工作代码。

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 %v\n", i+1, time.Now().Format("2006-01-02 15:04:05"))
       } else {
          fmt.Printf("Request %d rejected at %v\n", i+1, time.Now().Format("2006-01-02 15:04:05"))
       }
    }
    time.Sleep(2 * time.Second)
}

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

优点

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

缺点

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

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

2.4 令牌桶算法

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这是一个简单的实现案例,这个代码示例仅用于说明令牌桶算法的基本逻辑,并非完整的工作代码。
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
代码运行次数:0
运行
AI代码解释
复制
// 这是一个伪代码案例,演示实现逻辑
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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)
    })
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯技术工程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
分布式高并发系统限流原理与实践
随着业务的发展壮大,对后端服务的压力也会越来越大,为了打造高效稳定的系统, 产生了分布式,微服务等等系统设计,因为这个原因,设计复杂度也随之增加,基于此 诞生了高并发系统三大利器限流,缓存,降级/熔断。
merlinfeng
2020/12/31
9260
分布式高并发系统限流原理与实践
常用限流算法的应用场景和实现原理
在高并发业务场景下,保护系统时,常用的"三板斧"有:"熔断、降级和限流"。今天和大家谈谈常用的限流算法的几种实现方式,这里所说的限流并非是网关层面的限流,而是业务代码中的逻辑限流。
KevinYan
2020/12/31
1.2K0
常用限流算法的应用场景和实现原理
​# 加锁与令牌桶算法-限流设计对比
初始化时根据设定的最大令牌数(maxTokens)和补充间隔(refillInterval)自动计算出每次应补充的令牌数量,确保每秒补充的令牌总数精确等于最大容量。
GeekLiHua
2025/07/06
1320
​什么是限流,如何限流
某天小明突然发现自己的接口请求突然之间涨到了原来的10倍,接口几乎不能使用,产生了一系列连锁反应,导致了整个系统崩溃。这就好比,老电闸中都安装了保险丝,一旦使用大功率设备,保险丝就会熔断,保证各个电器不被强电流烧坏,系统也同样安装保险丝,防止非预期请求过大,引起系统瘫痪。
王小明_HIT
2021/05/20
3.3K0
Go实现常见的限流算法
该算法主要是会存在临界问题,如果流量都集中在两个窗口的交界处,那么突发流量会是设置上限的两倍。
每周聚焦
2025/05/07
1040
Go实现常见的限流算法
拒绝宕机!一文详解分布式限流方案(附代码实现)
随着微服务的流行,服务之间的依赖性和调用关系变得越来越复杂,服务的稳定性变得尤为重要。业务场景中经常会涉及到瞬时流量冲击,可能会导致请求响应超时,甚至服务器被压垮、宕机不可用。出于对系统本身和上下游服务的保护,我们通常会对请求进行限流处理,快速拒绝超出配置上限的请求,保证系统或上下游服务系统的稳定。合理策略能有效应对流量冲击,确保系统可用性和性能。本文详细介绍了几种限流算法,比较各个算法的优缺点,给出了限流算法选型的一些建议,同时对业务上常用的分布式限流也提出一些解决方案。
腾讯云开发者
2024/02/28
5.5K1
拒绝宕机!一文详解分布式限流方案(附代码实现)
Golang官方限流器的用法详解
限流器是提升服务稳定性的非常重要的组件,可以用来限制请求速率,保护服务,以免服务过载。限流器的实现方法有很多种,常见的限流算法有固定窗口、滑动窗口、漏桶、令牌桶,我在前面的文章 「常用限流算法的应用场景和实现原理」 中给大家讲解了这几种限流方法自身的特点和应用场景,其中令牌桶在限流的同时还可以应对一定的突发流量,与互联网应用容易因为热点事件出现突发流量高峰的特点更契合。
KevinYan
2021/07/15
6.4K0
基本限流算法与GuavaRateLimiter实现
总结: 固定窗口算法适用于对请求速率有明确要求且流量相对稳定的场景,但对于突发流量和请求分布不均匀的情况,可能需要考虑其他更灵活的限流算法。
leobhao
2024/08/07
2160
基本限流算法与GuavaRateLimiter实现
Spring Boot 的接口限流算法
在一个高并发系统中对流量的把控是非常重要的,当巨大的流量直接请求到我们的服务器上没多久就可能造成接口不可用,不处理的话甚至会造成整个应用不可用。
阿珍
2025/06/11
1290
Spring Boot 的接口限流算法
Go初级之http限流令牌桶的基本实现
此文是个人学习归纳的记录,腾讯云独家发布,未经允许,严禁转载,如有不对, 还望斧正, 感谢!
言志志
2024/04/25
3340
Go初级之http限流令牌桶的基本实现
golang 源码分析:juju/ratelimit
https://github.com/juju/ratelimit 是一个基于令牌桶算法的限流器:令牌桶就是想象有一个固定大小的桶,系统会以恒定速率向桶中放 Token,桶满则暂时不放。漏桶算法和令牌桶算法的主要区别在于,"漏桶算法"能够强行限制数据的传输速率(或请求频率),而"令牌桶算法"在能够限制数据的平均传输速率外,还允许某种程度的突发传输。
golangLeetcode
2022/08/02
8200
聊聊限流器TokenBucket的基本原理及实现
大家好,我渔夫子。上篇文章我们讲解了漏桶(LeakyBucket)的实现原理。本文我们介绍另外一种限流器---令牌桶(TokenBucket)。
Go学堂
2023/01/31
1.2K0
go-zero 是如何实现令牌桶限流的?
上一篇文章介绍了 如何实现计数器限流?主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码。
AlwaysBeta
2023/08/10
9280
后端服务不得不了解之限流
现在说到高可用系统,都会说到高可用的保护手段:缓存、降级和限流,本博文就主要说说限流。限流是流量限速(Rate Limit)的简称,是指只允许指定的事件进入系统,超过的部分将被拒绝服务、排队或等待、降级等处理。
好好学java
2021/02/07
1.8K0
高并发系统的限流策略:漏桶和令牌桶(附源码剖析)
漏桶算法比较好理解,假设我们现在有一个水桶,我们向这个水桶里添水,虽然我们我们无法预计一次会添多少水,也无法预计水流入的速度,但是可以固定出水的速度,不论添水的速率有多大,都按照固定的速率流出,如果桶满了,溢出的上方水直接抛弃。我们把水当作HTTP请求,每次都把请求放到一个桶中,然后以固定的速率处理请求,说了这么多,不如看一个图加深理解(图片来自于网络,手残党不会画,多多包涵):
Golang梦工厂
2022/07/08
1.1K0
高并发系统的限流策略:漏桶和令牌桶(附源码剖析)
超90%应用都在用的限流算法!究竟是什么套路轻松应对突发流量过载?细品...
前文,我们分享了限流算法中的固定窗口限流算法、滑动窗口算法、漏桶限流算法及它们的实践。今天分享一个实践中最常用的限流算法:令牌桶算法。
程序视点
2024/06/07
2250
超90%应用都在用的限流算法!究竟是什么套路轻松应对突发流量过载?细品...
Golang 标准库 限流器 time/rate 设计与实现
友情提示:此篇文章大约需要阅读 12分钟7秒,不足之处请多指教,感谢你的阅读。
Meng小羽
2020/08/25
2.5K0
Go每日一库之105:juju%2Fratelimit
限流又称为流量控制(流控),通常是指限制到达系统的并发请求数,常用的限流算法主要有漏洞和令牌桶。
luckpunk
2023/09/30
3870
Go 基于令牌桶的限流器
如果一般流量过大,下游系统反应不过来,这个时候就需要限流了,其实和上地铁是一样的,就是减慢上游访问下游的速度。
王小明_HIT
2021/11/12
4.4K0
Java - 深入四大限流算法:原理、实现与应用
Spring Boot - 利用Resilience4j-RateLimiter进行流量控制和服务降级
小小工匠
2024/05/25
1.1K0
Java - 深入四大限流算法:原理、实现与应用
相关推荐
分布式高并发系统限流原理与实践
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验