分布式限流三种方案详解

一、概述

在分布式系统中,限流是保障服务稳定性的重要手段。本文详细对比三种基于 Redis 的分布式限流方案:固定窗口滑动窗口令牌桶,帮助你在不同场景下做出合理的技术选型。


二、方案一:固定窗口(Fixed Window)

2.1 原理

将时间划分为固定长度的时间窗口(如 1 秒),每个窗口独立计数。窗口内计数器累加,超过阈值则拦截。

复制代码
时间轴:  |--- 第1秒 ---|--- 第2秒 ---|--- 第3秒 ---|
          ① ② ③ ④ ⑤ ⑥  ① ② ③      ① ② ③ ④ ⑤
          ↓              ↓            ↓
       计数=6          计数=3        计数=5
       阈值=5          阈值=5        阈值=5
       ❌ 拦截2个      ✅ 全部放行   ✅ 全部放行

2.2 Redis 实现(Lua 脚本)

lua 复制代码
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])

-- 原子性自增
local current = redis.call('INCR', key)

-- 首次访问设置过期时间
if current == 1 then
    redis.call('EXPIRE', key, ttl)
end

return current

2.3 优缺点

维度 评价
实现复杂度 ⭐ 极低,代码简洁
性能 ⭐⭐⭐ 最高,单次 Redis 调用
内存占用 ⭐⭐⭐ 最低,每个窗口只存一个计数器
流量均匀性 ⭐⭐ 存在边界突发问题

2.4 边界突发问题

复制代码
时间线:  |---- 第1秒 (阈值10) ----|---- 第2秒 (阈值10) ----|
请求:    第9、10个在 999ms 到达
          第1、2个在 1001ms 到达
          
结果:在 2ms 内通过了 12 个请求(超了 10 的限制)

2.5 适用场景

场景 是否适用 说明
API 防刷 ✅ 推荐 正常用户不会卡时间边界,边界问题可接受
登录限流 ✅ 推荐 防暴力破解,简单高效
IP 限流 ✅ 推荐 最常见的防刷场景
严格流量整形 ❌ 不推荐 边界突发不符合要求
秒杀/抢购 ⚠️ 慎用 边界突发可能导致不公平

2.6 代码示例

java 复制代码
@Component
public class FixedWindowRateLimiter {
    
    private static final String LUA_SCRIPT = 
        "local current = redis.call('INCR', KEYS[1])\n" +
        "if current == 1 then\n" +
        "    redis.call('EXPIRE', KEYS[1], ARGV[2])\n" +
        "end\n" +
        "return current";
    
    public boolean allow(String key, int limit, int windowSeconds) {
        Long count = redisTemplate.execute(
            new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
            Collections.singletonList(key),
            String.valueOf(limit),
            String.valueOf(windowSeconds)
        );
        return count != null && count <= limit;
    }
}

三、方案二:滑动窗口(Sliding Window)

3.1 原理

使用 Redis 的 Sorted Set(ZSET) 存储每个请求的时间戳,通过移除窗口外的旧数据,精确统计窗口内的请求数。

复制代码
时间轴:  |---- 过去1秒 ----|现在
          ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨
          ↓                 ↓
        ZSET 存储所有请求时间戳
        ZREMRANGEBYSCORE 移除窗口外数据
        ZCARD 统计窗口内数量

3.2 Redis 实现(Lua 脚本)

lua 复制代码
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])    -- 窗口大小(毫秒)
local limit = tonumber(ARGV[3])

-- 移除窗口外的旧数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count >= limit then
    return count  -- 拦截
end

-- 添加当前请求(使用毫秒时间戳 + 随机数作为 member,避免重复)
redis.call('ZADD', key, now, now .. ':' .. math.random())

-- 设置过期时间(窗口 + 1 秒)
redis.call('PEXPIRE', key, window + 1000)

return 0  -- 放行

3.3 优缺点

维度 评价
实现复杂度 ⭐⭐ 中等,需要理解 ZSET 操作
性能 ⭐⭐ 较高,ZREMRANGEBYSCORE + ZCARD + ZADD
内存占用 ⭐⭐ 较高,每个请求存一条记录
流量均匀性 ⭐⭐⭐ 最精确,无边界问题

3.4 滑动窗口精度示意

复制代码
固定窗口(边界突发):
  第1秒           第2秒
  |██████████|   |██████████|
  9 10    1 2    ← 2ms 内通过 12 个
  
滑动窗口(精确控制):
  |◄─── 1秒 ───►|◄─── 1秒 ───►|
  请求均匀分布,任意 1 秒窗口内 ≤ 阈值

3.5 适用场景

场景 是否适用 说明
严格 QPS 控制 ✅ 推荐 需要精确控制每秒请求数
金融交易限流 ✅ 推荐 对流量均匀性要求高
API 网关精确限流 ✅ 推荐 用户感知要求高
防刷场景 ⚠️ 可用 但固定窗口更简单,性价比更高
高并发场景 ⚠️ 慎用 内存占用随请求量线性增长

3.6 代码示例

java 复制代码
@Component
public class SlidingWindowRateLimiter {
    
    private static final String LUA_SCRIPT = 
        "local window = tonumber(ARGV[2])\n" +
        "local now = tonumber(ARGV[1])\n" +
        "local limit = tonumber(ARGV[3])\n" +
        "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)\n" +
        "local count = redis.call('ZCARD', KEYS[1])\n" +
        "if count >= limit then return count end\n" +
        "redis.call('ZADD', KEYS[1], now, now .. ':' .. math.random())\n" +
        "redis.call('PEXPIRE', KEYS[1], window + 1000)\n" +
        "return 0";
    
    public boolean allow(String key, int limit, int windowMs) {
        long now = System.currentTimeMillis();
        Long count = redisTemplate.execute(
            new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
            Collections.singletonList(key),
            String.valueOf(now),
            String.valueOf(windowMs),
            String.valueOf(limit)
        );
        return count != null && count == 0;
    }
}

四、方案三:令牌桶(Token Bucket)

4.1 原理

系统以固定速率向桶中放入令牌,每个请求需要消耗一个令牌。桶有容量上限,允许一定程度的突发流量。

复制代码
         ┌─────────────────────┐
         │   令牌桶 (容量 20)   │
         │  🪙🪙🪙🪙🪙🪙🪙🪙  │
         │  🪙🪙🪙🪙🪙🪙🪙🪙  │
         │  🪙🪙🪙🪙          │
         └──────────┬──────────┘
                    │
         ┌──────────▼──────────┐
         │   固定速率填充        │
         │   10 令牌/秒         │
         └─────────────────────┘

4.2 Redis 实现(Lua 脚本)

lua 复制代码
local key = KEYS[1]
local limit = tonumber(ARGV[1])      -- 桶容量
local rate = tonumber(ARGV[2])       -- 填充速率(令牌/秒)
local now = tonumber(ARGV[3])

-- 获取桶状态
local state = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(state[1]) or limit
local lastTime = tonumber(state[2]) or now

-- 计算应该补充的令牌数
local delta = math.max(0, now - lastTime)
local filledTokens = math.min(limit, tokens + (delta * rate / 1000))

-- 判断是否有足够令牌
if filledTokens >= 1 then
    -- 消耗 1 个令牌
    local newTokens = filledTokens - 1
    redis.call('HMSET', key, 'tokens', newTokens, 'last_time', now)
    redis.call('EXPIRE', key, 10)
    return 1  -- 放行
else
    -- 更新状态(不消耗)
    redis.call('HMSET', key, 'tokens', filledTokens, 'last_time', now)
    redis.call('EXPIRE', key, 10)
    return 0  -- 拦截
end

4.3 优缺点

维度 评价
实现复杂度 ⭐⭐⭐ 最高,需要维护桶状态
性能 ⭐⭐ 较高,HMGET + HMSET 多次操作
内存占用 ⭐⭐⭐ 低,仅存两个字段
流量均匀性 ⭐⭐⭐ 最平滑,允许可控突发

4.4 突发流量处理对比

复制代码
固定窗口:
  请求数
    ▲
  20│    ████████  (瞬间突发被拦截)
  10│  ████████
    └─────────────────► 时间

令牌桶:
  请求数
    ▲
  20│  ████████  (突发被平滑)
  10│  ████████
    └─────────────────► 时间

4.5 适用场景

场景 是否适用 说明
秒杀/抢购 ✅ 推荐 允许初期突发,平滑后续流量
消息队列消费 ✅ 推荐 控制消费速率,平滑处理
第三方 API 调用 ✅ 推荐 严格遵守对方限流规则
网关流量整形 ⚠️ 可用 功能强大但实现复杂,收益不高
简单防刷 ❌ 过度设计 固定窗口足够,没必要上令牌桶

4.6 代码示例

java 复制代码
@Component
public class TokenBucketRateLimiter {
    
    private static final String LUA_SCRIPT = 
        "local limit = tonumber(ARGV[1])\n" +
        "local rate = tonumber(ARGV[2])\n" +
        "local now = tonumber(ARGV[3])\n" +
        "local state = redis.call('HMGET', KEYS[1], 'tokens', 'last_time')\n" +
        "local tokens = tonumber(state[1]) or limit\n" +
        "local lastTime = tonumber(state[2]) or now\n" +
        "local delta = math.max(0, now - lastTime)\n" +
        "local filled = math.min(limit, tokens + (delta * rate / 1000))\n" +
        "if filled >= 1 then\n" +
        "    redis.call('HMSET', KEYS[1], 'tokens', filled - 1, 'last_time', now)\n" +
        "    redis.call('EXPIRE', KEYS[1], 10)\n" +
        "    return 1\n" +
        "end\n" +
        "redis.call('HMSET', KEYS[1], 'tokens', filled, 'last_time', now)\n" +
        "return 0";
    
    public boolean allow(String key, int capacity, int ratePerSecond) {
        long now = System.currentTimeMillis();
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
            Collections.singletonList(key),
            String.valueOf(capacity),
            String.valueOf(ratePerSecond),
            String.valueOf(now)
        );
        return result != null && result == 1;
    }
}

五、三种方案全景对比

维度 固定窗口 滑动窗口 令牌桶
实现复杂度 ⭐ 低 ⭐⭐ 中 ⭐⭐⭐ 高
性能 (Redis调用) 1 次 3 次 2 次
内存占用 ⭐⭐⭐ 低 (1个Key) ⭐⭐ 中 (N个成员) ⭐⭐⭐ 低 (2个字段)
流量均匀性 ⭐⭐ 边界突发 ⭐⭐⭐ 最精确 ⭐⭐⭐ 最平滑
允许突发 ❌ 突发即拦截 ❌ 突发即拦截 ✅ 可控突发
时间精度 秒级 毫秒级 毫秒级
Key 过期处理 自动过期 自动过期 需设置 TTL
适用场景 防刷、限流 精确控制 流量整形

性能基准测试(参考值)

方案 单次请求耗时 每秒处理能力
固定窗口 ~0.5ms 20000+
滑动窗口 ~1.2ms 8000+
令牌桶 ~1.0ms 10000+

六、选型决策树

复制代码
开始
  │
  ▼
是否需要严格均匀的流量分布?
  │
  ├── 是 ──► 是否允许突发流量?
  │           │
  │           ├── 是 ──► 令牌桶
  │           │
  │           └── 否 ──► 滑动窗口
  │
  └── 否 ──► 是否需要毫秒级精度?
              │
              ├── 是 ──► 滑动窗口
              │
              └── 否 ──► 固定窗口 ✅ 最推荐

七、场景化推荐

业务场景 推荐方案 理由
API 防刷 固定窗口 简单、高效、够用
IP 限流 固定窗口 最常见场景,性价比最高
登录暴力破解防护 固定窗口 3-5次/秒,固定窗口足够
秒杀/抢购 令牌桶 允许初期突发,平滑后续
消息队列消费 令牌桶 控制消费速率
第三方 API 调用 令牌桶 遵守对方限流规则
金融交易限流 滑动窗口 精确控制,无边界问题
严格 QPS 保证 滑动窗口 任意时刻都不超限
网关通用限流 固定窗口 性能最优,运维简单

八、总结

一句话选型

大部分场景选固定窗口,需要精确控制选滑动窗口,需要流量整形选令牌桶。

最终建议

优先级 方案 说明
首选 固定窗口 覆盖 80% 场景,简单可靠
按需 滑动窗口 对精度有严格要求时使用
慎用 令牌桶 功能强大但实现复杂,非必要不用

记住:过度设计是最大的敌人。先用最简单的方案解决问题,等真正遇到瓶颈再升级。

相关推荐
fīɡЙtīиɡ ℡19 小时前
分布式ID生成策略
java·分布式·算法
肥胖小羊21 小时前
企业微信应用的 Token 缓存策略与分布式锁高可用设计
分布式·缓存·企业微信
栩栩云生1 天前
被抹去的 27 秒:你的代码,其实并不活在真实的时间里
分布式·unix
刘小八1 天前
RabbitMQ 消息积压排查:从指标定位到消费者扩容
分布式·rabbitmq
风样滴男人哟1 天前
基于 .NET 开源、功能齐全的分布式作业调度系统
分布式·开源·.net
梅头脑2 天前
交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写
java·分布式
霸道流氓气质2 天前
分布式系统中接口时序不确定性处理
java·开发语言·分布式
YOU OU2 天前
Redis分布式锁
数据库·redis·分布式
爱浦路 IPLOOK2 天前
5GC实验室建设方案:面向5G核心网教学、研发与测试的标准化实验平台
分布式·科技·5g·架构·信息与通信