Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis分布式限流

Redis中分布式限流的三种实现方案详解

作者:流烟默

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

一、概述

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

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

2.1 原理

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

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

2.2 Redis 实现(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 代码示例

@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 脚本)

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 代码示例

@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 脚本)

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 代码示例

@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.5ms20000+
滑动窗口~1.2ms8000+
令牌桶~1.0ms10000+

六、选型决策树

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

七、场景化推荐

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

八、总结

一句话选型

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

最终建议

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

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

到此这篇关于Redis中分布式限流的三种实现方案详解的文章就介绍到这了,更多相关Redis分布式限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文