# 实现限流器

URL: https://caijiao.org/redis/09-use-cases/03-limiter
Source: docs/redis/09-use-cases/03-limiter.md
Description: 限流器（Rate Limiter）是为了控制访问频率，防止系统过载。本文介绍如何使用 Redis 实现限流器，确保系统稳定性与高可用性。

限流（Rate Limiting）是防止系统受到过高请求压力的重要策略，常用于 API 接口、Web 服务、微服务等场景。通过限流，我们可以控制请求的频率，避免系统因高并发请求而崩溃或性能下降。Redis 作为一个高性能的内存数据库，非常适合用来实现限流器，因为它能够快速处理大量的请求。

在本文中，我们将介绍如何使用 Redis 来实现一个简单的限流器。

## 1. 限流的常见算法

### 1.1 固定窗口计数法

固定窗口计数法是最简单的限流算法。它在固定的时间窗口内（如 1 分钟、1 小时等）统计请求的数量，一旦超过限制次数，则拒绝请求。每个时间窗口会从零开始重新计数。

**缺点**：这种算法容易出现临界情况，如 1 秒钟内同时触发两个时间窗口，会造成突发的请求通过，因此它的限流并不是非常平滑。

### 1.2 滑动窗口计数法

滑动窗口计数法是一种改进的限流算法，相比于固定窗口计数法，它采用的是一个不断滑动的时间窗口，每个请求在当前时间窗口的请求数会被记录，并在每个窗口滑动时更新。

**优点**：该算法能更平滑地分配请求，不容易出现突发的请求流量。

### 1.3 令牌桶算法

令牌桶算法是另一种常见的限流算法，每个请求都会从一个桶中取令牌。当桶中有令牌时，请求可以正常通过；当没有令牌时，请求将被拒绝。令牌桶有两个关键参数：令牌生成速率和桶的容量。令牌以固定的速率放入桶中，桶满时不再放令牌。

**优点**：可以处理突发流量。

### 1.4 漏桶算法

漏桶算法与令牌桶算法相似，不同之处在于请求会被放入一个固定大小的桶中，并按照固定速率处理请求。如果桶满了，新的请求会被丢弃。漏桶算法的特点是处理请求的速率是平稳的，突发流量会被丢弃。

## 2. 使用 Redis 实现限流器

### 2.1 使用固定窗口计数法实现限流器

在 Redis 中实现固定窗口计数法时，我们可以通过 `INCR` 和 `EXPIRE` 命令实现对请求计数的限制。

- 每个请求会先通过 `INCR` 命令增加计数器。
- 如果计数器超过了最大限制（例如每分钟最多 100 次请求），则拒绝该请求。
- 每个窗口会设置一个过期时间，当时间窗口结束时，计数器会自动清空。

#### 示例代码

假设我们想要限制每个用户每分钟最多请求 100 次 API，我们可以通过 Redis 来实现：

```python
import redis
import time

# 初始化 Redis 客户端
r = redis.StrictRedis(host='localhost', port=6379, db=0)

def is_request_allowed(user_id):
    # Redis 键名，使用用户ID和时间窗口来区分
    key = f"user:{user_id}:requests:{int(time.time() // 60)}"
    
    # INCR 命令递增请求次数
    request_count = r.incr(key)

    # 如果请求次数大于 100，拒绝请求
    if request_count > 100:
        return False

    # 设置过期时间为 60 秒（1分钟）
    if request_count == 1:
        r.expire(key, 60)

    return True

# 示例
user_id = "user123"
if is_request_allowed(user_id):
    print("请求被允许")
else:
    print("请求被拒绝：超出限制")
````

#### 说明

* 每个用户的请求次数都保存在 Redis 中，并根据时间窗口来命名 Redis 键（例如每分钟为一个窗口）。
* 每次请求时，`INCR` 命令增加当前时间窗口内的请求次数。如果请求次数超过了限制（100 次），就拒绝该请求。
* 通过 `EXPIRE` 命令为每个窗口设置过期时间，保证窗口内的数据不会无限增长。

### 2.2 使用令牌桶算法实现限流器

令牌桶算法的实现稍微复杂一点，因为它涉及到令牌的生成和存储。我们需要在 Redis 中维护一个令牌桶，定期向桶中放入令牌，当请求到来时检查桶中是否有令牌。

#### 示例代码

```python
import redis
import time

# 初始化 Redis 客户端
r = redis.StrictRedis(host='localhost', port=6379, db=0)

def is_request_allowed(user_id, max_tokens=5, refill_rate=1):
    # Redis 键名，使用用户ID来区分每个用户的令牌桶
    key = f"user:{user_id}:bucket"
    
    # 获取当前时间
    current_time = int(time.time())
    
    # 获取桶中的令牌数量和最后一次填充令牌的时间
    tokens, last_refill_time = r.mget(key, f"{key}:time")
    tokens = int(tokens) if tokens else max_tokens
    last_refill_time = int(last_refill_time) if last_refill_time else current_time

    # 计算令牌的填充数量
    elapsed_time = current_time - last_refill_time
    tokens += int(elapsed_time * refill_rate)

    # 令牌数不能超过最大容量
    if tokens > max_tokens:
        tokens = max_tokens

    # 检查是否有令牌
    if tokens > 0:
        # 如果有令牌，允许请求并扣除一个令牌
        r.set(key, tokens - 1)
        r.set(f"{key}:time", current_time)
        return True
    else:
        # 如果没有令牌，拒绝请求
        return False

# 示例
user_id = "user123"
if is_request_allowed(user_id):
    print("请求被允许")
else:
    print("请求被拒绝：令牌不足")
```

#### 说明

* 每次请求时，我们检查 Redis 中是否有可用的令牌。如果有令牌，则允许请求并扣除一个令牌；如果没有令牌，则拒绝请求。
* 令牌的生成速率由 `refill_rate` 控制（例如每秒 1 个令牌），最大令牌数由 `max_tokens` 控制（例如最大 5 个令牌）。
* 每次请求后，我们更新 Redis 中的令牌数量和最后一次填充令牌的时间。

### 2.3 使用滑动窗口算法实现限流器

滑动窗口算法实现起来较为复杂，需要定期更新每个时间段内的请求数量。通常，滑动窗口通过将每个时间窗口划分为多个子时间段来实现，它可以平滑地限制请求，而不会受到固定时间窗口的影响。

### 2.4 使用 Redis 实现分布式限流

在分布式系统中，限流不仅仅是针对单个用户或单个应用的操作，还需要在多个服务或多个实例之间同步限流。使用 Redis，可以通过设置全局限流器来协调多个实例的限流。

#### 示例

```python
def is_request_allowed_global(user_id, limit=100):
    # Redis 键名，全局限流器
    key = f"global_rate_limit:{user_id}"

    # 使用 Redis 的 INCR 命令递增请求次数
    request_count = r.incr(key)

    # 如果请求次数超过限制，拒绝请求
    if request_count > limit:
        return False

    # 设置过期时间为 1 分钟
    if request_count == 1:
        r.expire(key, 60)

    return True
```

## 3. 总结

限流是保证系统稳定性和高可用性的重要手段，Redis 提供了高效的内存存储，能够轻松实现各种限流算法，如固定窗口、滑动窗口、令牌桶等。使用 Redis 来实现限流器不仅可以降低数据库压力，还可以防止系统因超负载而崩溃。

通过合理的限流策略，我们能够确保系统在高并发情况下仍能保持正常的服务水平。
