Rate Limiter 业务限流处理

AI 摘要: 限流是为了保护系统,确保核心业务的可用性和稳定性。限流机制包括本地限流和分布式限流,其中基于Redis的分布式限流可以通过计数器、滑动窗口和令牌桶进行实现。最佳实践包括选择合适算法和存储、保持原子性、配置性、监控告警、灰度发布、与熔断降级结合、客户端适配和压测验证。

1. 限流\流控的本质

限流的本质是为了保护系统,防止过载或者针对恶意流量进行拦截,确保核心业务的可用性和稳定性。

2. 限流机制类型

2.1. 本地限流 (Local Rate Limiting)

单节点限流是指在单个服务实例内部进行的限流,不涉及跨服务实例的协调。它通常用于保护单个服务实例的及其或下游依赖资源。

2.2. 分布式限流

分布式限流是指在多个服务实例之间共享限流状态,以实现全局的限流。

基于 Redis 的分布式限流

设计:利用 Redis 的原子操作(如 INCR、SETNX、EXPIRE)来实现计数器、令牌桶或滑动窗口

  • 原理: 利用 Redis 的原子操作(如 INCR、SETNX、EXPIRE)来实现计数器、令牌桶或滑动窗口。
  • 优点: Redis 性能高,支持集群,易于部署和维护。
  • 实现方式:
    • 计数器: 使用 INCR 命令对某个 key 进行计数,并设置 EXPIRE。
    • 滑动窗口: 使用 Redis 的 ZSET(有序集合)存储请求的时间戳,通过 ZRANGEBYSCORE 和 ZREM 来维护窗口内的请求。
    • 令牌桶: 模拟令牌桶的逻辑,每次请求尝试从 Redis 中获取令牌(例如,一个 key 代表当前令牌数,通过 DECR 操作)。

基于 Redis 的分布式限流代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-redis/redis/v8"
)

// RedisLimiter implements a simple distributed fixed window counter limiter
type RedisLimiter struct {
	client *redis.Client
	key    string
	limit  int64
	window time.Duration
}

func NewRedisLimiter(client *redis.Client, key string, limit int64, window time.Duration) *RedisLimiter {
	return &RedisLimiter{
		client: client,
		key:    key,
		limit:  limit,
		window: window,
	}
}

func (rl *RedisLimiter) Allow(ctx context.Context) (bool, error) {
	// 使用Lua脚本保证原子性
	script := `
		local key = KEYS[1]
		local limit = tonumber(ARGV[1])
		local window = tonumber(ARGV[2])

		local current = redis.call('INCR', key)
		if current == 1 then
			redis.call('EXPIRE', key, window)
		end

		if current > limit then
			return 0
		else
			return 1
		end
	`
	res, err := rl.client.Eval(ctx, script, []string{rl.key}, rl.limit, int64(rl.window.Seconds())).Result()
	if err != nil {
		return false, fmt.Errorf("redis eval error: %w", err)
	}
	return res.(int64) == 1, nil
}

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // 替换为你的Redis地址
		DB:   0,
	})

	_, err := rdb.Ping(ctx).Result()
	if err != nil {
		log.Fatalf("Could not connect to Redis: %v", err)
	}
	fmt.Println("Connected to Redis!")

	limiter := NewRedisLimiter(rdb, "my_service_limit", 5, 10*time.Second) // 10秒内允许5个请求

	for i := 0; i < 10; i++ {
		allowed, err := limiter.Allow(ctx)
		if err != nil {
			fmt.Printf("Error checking limit for request %d: %v\n", i, err)
			continue
		}
		if allowed {
			fmt.Printf("Request %d allowed\n", i)
		} else {
			fmt.Printf("Request %d denied\n", i)
		}
		time.Sleep(500 * time.Millisecond)
	}

	// 等待窗口过期
	fmt.Println("Waiting for window to expire...")
	time.Sleep(10 * time.Second)

	fmt.Println("\n--- After window expiration ---")
	for i := 10; i < 15; i++ {
		allowed, err := limiter.Allow(ctx)
		if err != nil {
			fmt.Printf("Error checking limit for request %d: %v\n", i, err)
			continue
		}
		if allowed {
			fmt.Printf("Request %d allowed\n", i)
		} else {
			fmt.Printf("Request %d denied\n", i)
		}
		time.Sleep(500 * time.Millisecond)
	}
}

基于消息队列的分布式限流 (流量削峰)

典型场景,用户秒杀下单,将下单数据(用户、商品、数量等关键数据)推送到 MQ,再异步消费 MQ 数据处理复杂的下单业务

  • 原理: 将请求放入消息队列,后端消费者以恒定速率从队列中取出并处理。
  • 优点: 流量削峰填谷,保护后端系统。
  • 缺点: 引入异步处理,增加系统复杂性,对实时性要求高的场景不适用。
  • Go 实现: 使用 Kafka、RabbitMQ、NATS 等消息队列客户端。

基于 ZooKeeper/Etcd 的分布式限流

实际还是基于分布式选举获取一个锁,基于锁的数量判断师傅超额,还是有些复杂了

  • 原理: 利用 ZooKeeper/Etcd 的分布式锁或顺序节点特性来实现。例如,每个请求尝试获取一个分布式锁,或者在某个路径下创建临时顺序节点,通过节点数量来判断是否超限。
  • 优点: 强一致性,适用于对一致性要求较高的场景。
  • 缺点: 性能相对 Redis 低,延迟较高,不适合高并发场景。
  • Go 实现: 使用 go.etcd.io/etcd/client/v3 或 github.com/go-zookeeper/zk。

本地+分布式两者结合

针对非常大流量网站,之前的业务中有做过本地+分布式限流的结合

  1. 节点本地限流拦截一道
  2. 再走分布式限流拦截一道

3. 限流策略实现

3.1. 令牌桶 (Token Bucket)

令牌入桶,桶口令牌消耗随请求流量可大可小,消耗光了则阻塞等待新的入桶令牌

  • 原理: 桶中以恒定速率放入令牌,请求需要消耗桶令牌才能被处理,如果桶中没有令牌,请求将被拒绝或排队
  • 优点:桶支持囤积一定令牌,所以允许一定程度的突发流量,平滑请求速率
  • Go 实现:
    • 使用time.Tickertime.After定时生成令牌。
    • 使用 sync.Mutex chan 保护令牌桶的并发访问。

令牌桶代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

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

type TokenBucket struct {
	capacity    int64         // 桶的容量
	rate        int64         // 每秒生成的令牌数
	tokens      int64         // 当前桶中的令牌数
	lastRefill  time.Time     // 上次补充令牌的时间
	mu          sync.Mutex
}

func NewTokenBucket(capacity, rate int64) *TokenBucket {
	return &TokenBucket{
		capacity:   capacity,
		rate:       rate,
		tokens:     capacity, // 初始时桶是满的
		lastRefill: time.Now(),
	}
}

func (tb *TokenBucket) Allow() bool {
	tb.mu.Lock()
	defer tb.mu.Unlock()

	now := time.Now()
	// 计算需要补充的令牌数
	tokensToAdd := (now.UnixNano() - tb.lastRefill.UnixNano()) * tb.rate / int64(time.Second)
	tb.tokens = min(tb.capacity, tb.tokens+tokensToAdd)
	tb.lastRefill = now

	if tb.tokens >= 1 {
		tb.tokens--
		return true
	}
	return false
}

func min(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}

func main() {
	bucket := NewTokenBucket(10, 2) // 容量10,每秒生成2个令牌

	for i := 0; i < 20; i++ {
		if bucket.Allow() {
			fmt.Printf("Request %d allowed\n", i)
		} else {
			fmt.Printf("Request %d denied\n", i)
		}
		time.Sleep(200 * time.Millisecond) // 模拟请求间隔
	}
}

3.2. 漏桶 (Leaky Bucket)

令牌入桶,桶口大小固定,每间隔 T 时间间隔从桶口漏一个令牌,请求消耗令牌

  • 原理: 请求以恒定速率从桶中流出,如果桶满,新请求将被拒绝
  • 优点: 强制输出速率恒定,对下游系统有很好的保护作用
  • Go 实现: 通常使用带缓冲的 chan 模拟漏桶,或者结合 time.Ticker 控制出队速率

漏桶策略代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
	"fmt"
	"time"
)

type LeakyBucket struct {
	capacity int               // 桶的容量
	outRate  time.Duration // 漏出速率,每个请求流出的时间间隔
	requests chan struct{} // 模拟桶
}

func NewLeakyBucket(capacity int, outRate time.Duration) *LeakyBucket {
	lb := &LeakyBucket{
        capacity: capacity,
		outRate:  outRate,
		requests: make(chan struct{}, capacity),
	}
	go lb.leak() // 启动漏出协程
	return lb
}

func (lb *LeakyBucket) leak() {
	ticker := time.NewTicker(lb.outRate)
	defer ticker.Stop()
	for range ticker.C {
		select {
		case <-lb.requests:
			// 成功漏出一个请求
		default:
			// 桶为空,不漏出
		}
	}
}

func (lb *LeakyBucket) Allow() bool {
	select {
	case lb.requests <- struct{}{}: // 尝试放入请求
		return true
	default: // 桶已满
		return false
	}
}

func main() {
	bucket := NewLeakyBucket(5, 500*time.Millisecond) // 容量5,每500ms漏出一个请求

	for i := 0; i < 15; i++ {
		if bucket.Allow() {
			fmt.Printf("Request %d accepted\n", i)
		} else {
			fmt.Printf("Request %d rejected\n", i)
		}
		time.Sleep(100 * time.Millisecond) // 模拟请求到达
	}
}

3.3. 计数器 (Fixed Window Counter)

  • 原理: 在一个固定时间窗口内,统计请求数量,达到阈值则拒绝。
  • 优点: 实现简单。
  • 缺点: 存在“临界问题”,即在窗口边缘可能允许两倍的流量
  • Go 实现: Redis 的 ADD 操作,超过阈值拒绝,下个 T 清空计数器,重新累加

令牌和计数器区别

  • 计数器有阈值设置,累加达到阈值后,需要在下个 T 时间后置空,可能有两倍流量问题;
  • 令牌可以有初始容量,消耗了令牌后,还可以在每间隔 T 时间后有流入的令牌(稍微缓和一些,不会有两倍流量问题) – 细水长流

3.4. 滑动窗口计数器 (Sliding Window Counter)

更加平滑的限流

  • 原理: 将时间窗口划分为更小的子窗口,每个子窗口维护一个计数器。通过滑动窗口,可以更平滑地统计请求数量,避免固定窗口的临界问题。
  • 优点: 解决了固定窗口的临界问题,更精确。
  • Go 实现: 需要维护一个队列或环形缓冲区来存储子窗口的计数。

4. 开源解决方案

4.1. 一些限流开源库

alibaba/sentinel-golang的特性功能比较多,go-redis/redis_rate非常简单

  • golang.org/x/time/rate: Go 官方提供的限流库,实现了令牌桶算法,功能强大且易于使用
  • uber-go/ratelimit: Uber 开源的 Go 语言限流库,提供了漏桶速率限制算法的 Golang 实现
  • go-redis/redis_rate: 基于 go-redis 的 Redis 分布式限流库,提供了令牌桶和滑动窗口的实现
  • alibaba/sentinel-golang: 阿里巴巴开源的流量控制组件 Sentinel 的 Go 语言实现。它提供了熔断、降级、限流等多种流量治理能力,支持分布式限流(通过集成外部存储如 Redis)。
    • 特点: 规则动态配置、实时监控、多种限流模式(QPS、并发线程数)、流控效果(快速失败、排队等待)。
    • 分布式限流实现: Sentinel 本身是单机限流,但可以通过扩展数据源(如 Redis)来实现分布式限流。例如,将限流规则和计数器存储在 Redis 中,各个 Sentinel 实例从 Redis 获取并更新状态

uber-go/ratelimit 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import (
	"fmt"
	"time"

	"go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(100) // per second

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take()
        fmt.Println(i, now.Sub(prev))
        prev = now
    }

    // Output:
    // 0 0
    // 1 10ms
    // 2 10ms
    // 3 10ms
    // 4 10ms
    // 5 10ms
    // 6 10ms
    // 7 10ms
    // 8 10ms
    // 9 10ms
}

go-redis/redis_rate 示例

非常简洁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package redis_rate_test

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
	"github.com/go-redis/redis_rate/v10"
)

func ExampleNewLimiter() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
	_ = rdb.FlushDB(ctx).Err()

	limiter := redis_rate.NewLimiter(rdb)
	res, err := limiter.Allow(ctx, "project:123", redis_rate.PerSecond(10))
	if err != nil {
		panic(err)
	}
	fmt.Println("allowed", res.Allowed, "remaining", res.Remaining)
	// Output: allowed 1 remaining 9
}

5. API 网关集中式限流

将所有服务的限流逻辑集中到一个独立的限流服务中。所有请求在进入业务服务之前,都需要先经过限流服务进行校验,常见就是接入层网关、业务网关等

常见方式有:

  1. API Gateway/Sidecar 模式

5.1. API Gateway/Sidecar 模式

  • 原理: 在 API Gateway 层或通过 Sidecar 代理模式,拦截所有请求,并在该层进行统一的限流判断。
  • 优点:
    • 统一管理: 所有限流规则集中管理,易于配置和维护。
    • 透明性: 对业务服务无侵入。
    • 全局视角: 可以实现更复杂的全局限流策略(如用户维度、IP 维度、API 维度)。
  • Go 实现:
    • API Gateway: 使用 Go 语言开发一个高性能的 API Gateway,集成限流逻辑。例如,基于 Gin、Echo 等 Web 框架,结合上述分布式限流方案。
    • Envoy Proxy + Go Filter: Envoy 是一个高性能的 L7 代理,支持通过 Wasm 或外部 GRPC 服务扩展其功能。可以编写 Go 语言的 GRPC 服务作为 Envoy 的外部授权服务或限流服务,Envoy 将请求转发给该服务进行限流判断。
    • Kong/APISIX + Go Plugin: Kong 和 APISIX 是流行的 API Gateway,支持通过插件扩展功能。可以编写 Go 语言插件来实现自定义限流逻辑。
    • 自研限流服务: 如果业务需求非常特殊,可以考虑使用 Go 语言自研一个高性能的限流服务,对外提供 GRPC 或 HTTP 接口供其他服务调用,该服务内部可以采用 Redis 等作为状态存储

6. 常见限流策略 (Rate Limiting Strategies)

6.1. 基于 QPS (Queries Per Second) / TPS (Transactions Per Second)

最常见的限流方式,限制单位时间内允许的请求数量,适用场景: 大多数 API 接口、服务入口。

6.2. 基于并发连接数/线程数

限制同时处理的请求数量,适用场景: 数据库连接池、RPC 调用下游服务、CPU 密集型任务。

Go 实现: 使用带缓冲的 chan 作为信号量,或者 semaphore 库。

使用 semaphore 示例

PS. 实际和带缓冲通道处理起来很类似~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"golang.org/x/sync/semaphore"
)

func main() {
	// 限制并发数为3
	sem := semaphore.NewWeighted(3)
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			// 尝试获取一个许可
			if err := sem.Acquire(context.Background(), 1); err != nil {
				fmt.Printf("Request %d: Failed to acquire semaphore: %v\n", id, err)
				return
			}
			defer sem.Release(1) // 释放许可

			fmt.Printf("Request %d: Processing...\n", id)
			time.Sleep(1 * time.Second) // 模拟业务处理
			fmt.Printf("Request %d: Done.\n", id)
		}(i)
	}
	wg.Wait()
	fmt.Println("All requests processed.")
}

6.3. 其他限流策略

  1. 基于用户 ID/IP 地址,例如登录场景,根据请求的 User-ID 或 IP 作为限流 key,防止恶意攻击或滥用
  2. 基于 API 接口/资源路径,例如一些资源接口,需要根据请求的 URL 路径或方法作为限流 key,对不同的 API 接口设置不同的限流阈值。
  3. 基于系统负载/资源利用率(类似动态调节): 需要实时监控系统指标,并根据指标动态更新限流规则。这通常需要结合监控系统(如 Prometheus)和配置中心
  4. 多维度组合限流: 例如之前提到的单节点本地限流+分布式限流结合

6.4. 限流后的处理方式 (Flow Control Effects)

当请求被限流时,如何处理这些请求也很重要,包括:

  1. 快速失败 (Fail Fast): 直接拒绝请求,返回错误码(如 HTTP 429 Too Many Requests),简单直接,快速释放资源,适用大多数同步 API
  2. 排队等待 (Queueing): 将请求放入队列,等待有可用资源时再处理,例如下单处理
    • 优点: 提高请求成功率,平滑流量
    • 缺点: 增加请求延迟,需要考虑队列溢出和超时
    • Go 实现: 使用带缓冲的 chan 或自定义队列
  3. 降级 (Degradation):当流量过大时,返回一个默认值、缓存数据或简化版的服务响应,而不是拒绝请求
    • 适用场景: 非核心功能、推荐系统、搜索结果等
    • 优点: 保证核心功能可用性,提升用户体验
  4. 重试 (Retry):注意重试可能引发雪崩
    • 客户端收到限流错误后,在一定延迟后进行重试
    • 优点: 提高请求成功率。
    • 缺点: 可能加剧系统压力,需要实现指数退避等策略。

7. 一个业务限流的实例

典型 Case: 在用户登录场景,需要对登录的用户 UID 进行限频,防止用户恶意攻击

几个关键点:

  1. 使用 Redis+Lua 脚本确保了原子操作
  2. 借助Redis的SZET有序集合实现,对应的Key表示限流实体(比如某个用户登录),针对SET集合中每个元素设置+T的过期时间,统计未过期集合中元素个数与阈值比较确定是否被限流了
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
package rate_limiter

import (
    "context"
    "fmt"
    "time"

    "github.com/google/uuid"
    "github.com/redis/go-redis/v9"
)

// LoginRateLimiter 定义登录限流器
type LoginRateLimiter struct {
    client *redis.Client
    prefix string        // Redis key 前缀,例如 "rate_limit:login:"
    limit  int64         // 允许在窗口期内的最大请求数
    window time.Duration // 限流窗口大小,例如 1分钟
}

// NewLoginRateLimiter 创建一个新的登录限流器实例
func NewLoginRateLimiter(client *redis.Client, prefix string, limit int64, window time.Duration) *LoginRateLimiter {
    return &LoginRateLimiter{
        client: client,
        prefix: prefix,
        limit:  limit,
        window: window,
    }
}

// Allow 检查给定UID的登录请求是否被允许
// 返回值:
//   - bool: true表示允许,false表示拒绝
//   - int64: 当前窗口内的请求数
//   - error: 如果发生Redis错误
func (lrl *LoginRateLimiter) Allow(ctx context.Context, uid string) (bool, int64, error) {
    key := lrl.prefix + uid
    now := time.Now().UnixNano() // 使用纳秒时间戳作为score,保证唯一性
    minScore := now - lrl.window.Nanoseconds()

    // 使用Lua脚本保证原子性
    // 脚本逻辑:
    // 1. 移除ZSET中所有过期的时间戳 (score < minScore)
    // 2. 将当前请求的时间戳加入ZSET
    // 3. 获取ZSET中当前元素的数量
    // 4. 设置ZSET的过期时间 (可选,作为兜底,因为我们会不断清理)
    script := `
		local key = KEYS[1]
		local minScore = ARGV[1]
		local now = ARGV[2]
		local member = ARGV[3]
		local limit = tonumber(ARGV[4])
		local expireTime = tonumber(ARGV[5]) -- 窗口时间(秒)

		-- 1. 移除过期时间戳
		redis.call('ZREMRANGEBYSCORE', key, '-inf', minScore)

		-- 2. 添加当前请求时间戳
		redis.call('ZADD', key, now, member)

		-- 3. 获取当前ZSET中的元素数量
		local currentCount = redis.call('ZCARD', key)

		-- 4. 设置过期时间,防止key长期占用内存,但每次操作都会刷新TTL
		-- 考虑到ZSET会不断被清理,这个TTL更多是作为兜底
		redis.call('EXPIRE', key, expireTime)

		if currentCount > limit then
			return {0, currentCount} -- 0表示拒绝
		else
			return {1, currentCount} -- 1表示允许
		end
	`

    // 生成一个唯一的member,防止同一纳秒内有多个请求导致member重复
    member := fmt.Sprintf("%d-%s", now, uuid.NewString())

    // 执行Lua脚本
    result, err := lrl.client.Eval(ctx, script, []string{key}, minScore, now, member, lrl.limit, int64(lrl.window.Seconds())).Result()
    if err != nil {
        return false, 0, fmt.Errorf("redis eval error: %w", err)
    }

    // 解析Lua脚本的返回结果
    // Lua脚本返回的是一个数组 {allow_flag, current_count}
    resSlice, ok := result.([]interface{})
    if !ok || len(resSlice) != 2 {
        return false, 0, fmt.Errorf("unexpected redis script result format: %v", result)
    }

    allowFlag, ok := resSlice[0].(int64)
    if !ok {
        return false, 0, fmt.Errorf("unexpected allow flag type: %v", resSlice[0])
    }

    currentCount, ok := resSlice[1].(int64)
    if !ok {
        return false, 0, fmt.Errorf("unexpected current count type: %v", resSlice[1])
    }

    return allowFlag == 1, currentCount, nil
}

8. 总结与最佳实践

  • 选择合适的算法: 令牌桶和滑动窗口是分布式限流的常用选择,它们在平滑性和精确性上表现良好
  • 选择合适的存储: Redis 是分布式限流的首选,其高性能和原子操作非常适合
  • Lua 脚本原子性: 在 Redis 中实现复杂限流逻辑时,务必使用 Lua 脚本来保证操作的原子性,避免竞态条件。
  • 可配置性: 限流规则应该可配置,最好支持动态更新,无需重启服务。
  • 监控与告警: 实时监控限流效果,包括被拒绝的请求数量、当前流量等,并设置告警。
  • 灰度发布: 新的限流策略上线前,进行灰度发布,逐步验证效果。
  • 与熔断、降级结合: 限流是流量控制的第一道防线,但它不能解决所有问题。结合熔断(防止雪崩效应)和降级(保证核心功能可用)可以构建更健壮的系统。
  • 客户端适配: 告知客户端限流策略,并建议客户端实现重试、指数退避等机制。
  • 压测验证: 在生产环境上线前,进行充分的压力测试,验证限流效果和系统稳定性。