分布式限流三种方案详解

一、概述

在分布式系统中,限流是保障服务稳定性的重要手段。本文详细对比三种基于 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% 场景,简单可靠
按需 滑动窗口 对精度有严格要求时使用
慎用 令牌桶 功能强大但实现复杂,非必要不用

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

相关推荐
大菠萝爱上小西瓜18 小时前
【大数据实战】Spark 2.2.2 完全分布式集群搭建
大数据·分布式·spark
肠畔码农18 小时前
深度解析 RocketMQ 消费端限流与重平衡(Rebalance):分布式队列分配与流控防雪崩本质
分布式·rocketmq
海兰19 小时前
【 Kafka进阶3】Apache Kafka 分布式事件流平台:架构原理与微服务解耦机制简要分析
分布式·架构·kafka
麻瓜记录Jackson2 天前
深入剖析 ZooKeeper 分布式 互斥锁:Curator 之 InterProcessMutex 源码全解(附图解)
java·spring boot·分布式·后端·zookeeper·java-zookeeper
Keystone_Onion2 天前
跨境电商架构演进:基于美国海外仓的中大件分布式仓网路由优化方案
分布式·架构
盛世宏博智慧档案2 天前
分布式机房环境监测硬件,以太网温湿度采集终端如何应用
分布式·传感器·机房·温湿度
NJCloud2 天前
ELK企业级日志分析平台(四)——基于 ELFK + Kafka 的日志采集与传输平台部署实践
linux·运维·分布式·elk·kafka
国科安芯2 天前
星载网络化控制的总线脊梁:四路CANFD在分布式载荷管理中的架构优势
分布式·单片机·嵌入式硬件·架构·系统架构·canfd·低轨卫星星座
Allen_LVyingbo2 天前
分布式GPU智能算法深度研究2026年版(下)
人工智能·分布式·算法·机器学习·知识图谱
何以解忧,唯有..2 天前
Redisson分布式锁实现原理深度解析
分布式