Redis+Lua限流的四种算法

1. 固定窗口(Fixed Window)

原理:
  • 固定窗口算法将时间划分为固定的时间段(窗口),比如 1 秒、1 分钟等。在每个时间段内,允许最多一定数量的请求。如果请求超出配额,则拒绝。
优点:
  • 实现简单,能够快速处理请求限流。
缺点:
  • 在窗口边界处可能出现流量突增的情况(称为"边界效应"),比如两个窗口交界处可能短时间内允许通过的请求数量翻倍。
Lua脚本:
lua 复制代码
-- KEYS[1]: 限流的键(通常为用户ID或者API)
-- ARGV[1]: 最大允许请求数
-- ARGV[2]: 窗口时间(以秒为单位)

local current = redis.call('GET', KEYS[1])

if current and tonumber(current) >= tonumber(ARGV[1]) then
    return 0  -- 返回0表示超出限流
else
    current = redis.call('INCR', KEYS[1])
    if tonumber(current) == 1 then
        redis.call('EXPIRE', KEYS[1], ARGV[2])  -- 设置窗口时间
    end
    return 1  -- 返回1表示未超限
end
Java模拟限流:
java 复制代码
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;


/**
 * 固定窗口lua限流Demo
 *
 * @author strap
 */
public class FixedWindowExample {

    private static final String LIMIT_SCRIPT =
            "-- KEYS[1]: 限流的键(通常为用户ID或者API)\n" +
                    "-- ARGV[1]: 最大允许请求数\n" +
                    "-- ARGV[2]: 窗口时间(以秒为单位)\n" +
                    "\n" +
                    "local current = redis.call('GET', KEYS[1])\n" +
                    "\n" +
                    "if current and tonumber(current) >= tonumber(ARGV[1]) then\n" +
                    "    return 0  -- 返回0表示超出限流\n" +
                    "else\n" +
                    "    current = redis.call('INCR', KEYS[1])\n" +
                    "    if tonumber(current) == 1 then\n" +
                    "        redis.call('EXPIRE', KEYS[1], ARGV[2])  -- 设置窗口时间\n" +
                    "    end\n" +
                    "    return 1  -- 返回1表示未超限\n" +
                    "end";

    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "FixedWindowExample", "10", "5");
                if (Long.valueOf(1).equals(o)) {
                    System.out.println(i + "=============================放行");
                } else {
                    System.out.println(i + "拦截=============================");
                }
            }
        }
    }

}

2. 滑动窗口(Sliding Window)

原理:
  • 滑动窗口改进了固定窗口的"边界效应"问题,它通过更细粒度的时间单位来平滑地控制请求。滑动窗口可以在较短的时间窗口内动态调整请求计数,防止瞬时流量激增。
优点:
  • 能平滑地限制请求,减少流量的突增问题。
缺点:
  • 相对固定窗口来说,滑动窗口实现复杂度更高。
Lua脚本:
lua 复制代码
-- KEYS[1]: 限流的键(通常为用户ID或者API)
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 最大允许请求数
-- ARGV[3]: 当前时间戳(毫秒)

-- 移除窗口外的请求
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1] * 1000)

local count = redis.call('ZCARD', KEYS[1])

if tonumber(count) >= tonumber(ARGV[2]) then
    return 0  -- 请求被限制
else
    redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])  -- 添加当前请求的时间戳
    redis.call('EXPIRE', KEYS[1], ARGV[1])  -- 设置过期时间
    return 1  -- 请求允许
end
Java模拟限流:
java 复制代码
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;


/**
 * 滑动窗口lua限流Demo
 *
 * @author strap
 */
public class SlidingWindowExample {

    private static final String LIMIT_SCRIPT =
            "-- KEYS[1]: 限流的键(通常为用户ID或者API)\n" +
                    "-- ARGV[1]: 时间窗口(秒)\n" +
                    "-- ARGV[2]: 最大允许请求数\n" +
                    "-- ARGV[3]: 当前时间戳(毫秒)\n" +
                    "\n" +
                    "-- 移除窗口外的请求\n" +
                    "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1] * 1000)\n" +
                    "\n" +
                    "local count = redis.call('ZCARD', KEYS[1])\n" +
                    "\n" +
                    "if tonumber(count) >= tonumber(ARGV[2]) then\n" +
                    "    return 0  -- 请求被限制\n" +
                    "else\n" +
                    "    redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])  -- 添加当前请求的时间戳\n" +
                    "    redis.call('EXPIRE', KEYS[1], ARGV[1])  -- 设置过期时间\n" +
                    "    return 1  -- 请求允许\n" +
                    "end";


    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "SlidingWindowExample", "5", "10", now + "");
                if (Long.valueOf(1).equals(o)){
                    System.out.println(i + "=============================放行");
                }else {
                    System.out.println(i + "拦截=============================");
                }
            }

        }
    }


}

3. 令牌桶(Token Bucket)

原理:
  • 令牌桶算法以恒定速率向桶中添加令牌。每次请求需要消耗一定数量的令牌,如果桶内有足够的令牌,允许请求通过;否则拒绝请求。令牌可以积累,从而允许短时间内的流量突发。
优点:
  • 允许短时间的流量突发,适用于需要应对高峰流量的场景。
缺点:
  • 如果高峰流量持续时间较长,可能导致后续请求被大量拒绝。
Lua脚本:
lua 复制代码
-- 当前的键
local key = KEYS[1]
-- 令牌桶的容量
local capacity = tonumber(ARGV[1])
-- 令牌的生成速率(个/秒)
local rate = tonumber(ARGV[2])
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[3])
-- 请求的令牌数量
local requestedTokens = tonumber(ARGV[4])
-- 键的最大生命周期
local expire = math.ceil(capacity / rate)

-- 获取当前桶内的令牌数量,默认为capacity
local currentTokens = tonumber(redis.call('HGET', key, 'currentTokens') or capacity)
-- 获取上次令牌更新的时间
local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or 0)

-- 首次进来初始化令牌数量
if lastUpdate == 0 then
    redis.call('HSET', key, 'last_update', now)
    redis.call('HSET', key, 'currentTokens', currentTokens)
    redis.call('EXPIRE', key, expire)
else
    -- 计算在当前时间段内生成的令牌数量
    local tokensToAdd = math.floor((now - lastUpdate) / 1000 * rate)
    currentTokens = math.min(capacity, currentTokens + tokensToAdd)
end

-- 计算当前是否能提供请求的令牌数量
local isAllow = 0
if currentTokens >= requestedTokens then
    isAllow = 1
    redis.call('HSET', key, 'last_update', now)
    redis.call('HSET', key, 'currentTokens', currentTokens - requestedTokens)
    redis.call('EXPIRE', key, expire)
end

return {isAllow, currentTokens}
Java模拟限流:
java 复制代码
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;

import java.util.List;

/**
 * 令牌桶lua限流Demo
 *
 * @author strap
 */
public class TokenBucketExample {

    private static final String LIMIT_SCRIPT = "-- 当前的键\n" +
            "local key = KEYS[1]\n" +
            "-- 令牌桶的容量\n" +
            "local capacity = tonumber(ARGV[1])\n" +
            "-- 令牌的生成速率(个/秒)\n" +
            "local rate = tonumber(ARGV[2])\n" +
            "-- 当前时间戳(毫秒)\n" +
            "local now = tonumber(ARGV[3])\n" +
            "-- 请求的令牌数量\n" +
            "local requestedTokens = tonumber(ARGV[4])\n" +
            "-- 键的最大生命周期\n" +
            "local expire = math.ceil(capacity / rate)\n" +
            "\n" +
            "-- 获取当前桶内的令牌数量,默认为capacity\n" +
            "local currentTokens = tonumber(redis.call('HGET', key, 'currentTokens') or capacity)\n" +
            "-- 获取上次令牌更新的时间\n" +
            "local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or 0)\n" +
            "\n" +
            "-- 首次进来初始化令牌数量\n" +
            "if lastUpdate == 0 then\n" +
            "    redis.call('HSET', key, 'last_update', now)\n" +
            "    redis.call('HSET', key, 'currentTokens', currentTokens)\n" +
            "    redis.call('EXPIRE', key, expire)\n" +
            "else\n" +
            "    -- 计算在当前时间段内生成的令牌数量\n" +
            "    local tokensToAdd = math.floor((now - lastUpdate) / 1000 * rate)\n" +
            "    currentTokens = math.min(capacity, currentTokens + tokensToAdd)\n" +
            "end\n" +
            "\n" +
            "-- 计算当前是否能提供请求的令牌数量\n" +
            "local isAllow = 0\n" +
            "if currentTokens >= requestedTokens then\n" +
            "    isAllow = 1\n" +
            "    redis.call('HSET', key, 'last_update', now)\n" +
            "    redis.call('HSET', key, 'currentTokens', currentTokens - requestedTokens)\n" +
            "    redis.call('EXPIRE', key, expire)\n" +
            "end\n" +
            "\n" +
            "return {isAllow, currentTokens}";

    @SneakyThrows
    public static void main(String[] args) {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            int rate = 1;       // 速率(令牌的生成速率(个/秒))
            int capacity = 10;  // 桶容量(令牌桶的容量)
            int everyTime = 1;  // 每次请求需要拿走的令牌数量
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "TokenBucketExample", String.valueOf(capacity), String.valueOf(rate), String.valueOf(now), String.valueOf(everyTime));
                List<Object> resutl = (List)o;
                if (Long.valueOf(1).equals(resutl.get(0))){
                    System.out.println(i + "请求前桶内剩余令牌数:" + resutl.get(1) + "==============================放行");
                }else {
                    System.out.println(i + "请求前桶内剩余令牌数:" + resutl.get(1) + "=拦截=============================");
                }
            }
        }

    }


}

4.漏桶(Leaky Bucket)

原理:
  • 漏桶算法将请求流量放入一个"漏桶"中,桶以固定速率漏水(处理请求)。如果流量超过桶的容量,多余的请求将被拒绝。漏桶严格控制输出速率,因此不会出现流量突发。
优点:
  • 严格限制请求速率,适用于要求平滑流量的场景。
缺点:
  • 不允许流量突发,处理效率可能不及令牌桶。
Lua脚本:
lua 复制代码
-- 当前的键
local key = KEYS[1]
-- 漏桶的容量
local capacity = tonumber(ARGV[1])
-- 漏水速率(个/秒)
local rate = tonumber(ARGV[2])
-- 当前时间戳
local now = tonumber(ARGV[3])
-- 请求计数(进来的tokens数量)
local requestedTokens = tonumber(ARGV[4])
-- 键的最大生命周期
local expire = math.ceil(capacity / rate)

-- 获取当前漏桶内的令牌数量,默认为0
local currentTokens = tonumber(redis.call('HGET', key, 'tokens') or 0)
-- 获取上次漏桶令牌数量的更新时间
local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or now)
-- 漏桶在当前时间范围内已流出的令牌数
local leaks = math.floor((now - lastUpdate) / 1000 * rate)
-- 重新计算当前漏桶内的令牌数量 math.min(capacity, currentTokens + deltaTokens)
currentTokens = math.max(currentTokens - leaks + requestedTokens, 0)
-- 是否允许通过,默认不允许
local isAllow = 0
if currentTokens <= capacity then
    -- 当前令牌数量还能放进去
    isAllow = 1
    redis.call('HSET', key, 'tokens', currentTokens)
    redis.call('HSET', key, 'last_update', now)
    redis.call('EXPIRE', key, expire);
end
return {isAllow, currentTokens}
Java模拟限流:
java 复制代码
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;

import java.util.List;


/**
 * 漏桶lua限流Demo
 *
 * @author strap
 */
public class LeakyBucketExample {

    private static final String LIMIT_SCRIPT =
            "-- 当前的键\n" +
                    "local key = KEYS[1]\n" +
                    "-- 漏桶的容量\n" +
                    "local capacity = tonumber(ARGV[1])\n" +
                    "-- 漏水速率(个/秒)\n" +
                    "local rate = tonumber(ARGV[2])\n" +
                    "-- 当前时间戳\n" +
                    "local now = tonumber(ARGV[3])\n" +
                    "-- 请求计数(进来的tokens数量)\n" +
                    "local requestedTokens = tonumber(ARGV[4])\n" +
                    "-- 键的最大生命周期\n" +
                    "local expire = math.ceil(capacity / rate)\n" +
                    "\n" +
                    "-- 获取当前漏桶内的令牌数量,默认为0\n" +
                    "local currentTokens = tonumber(redis.call('HGET', key, 'tokens') or 0)\n" +
                    "-- 获取上次漏桶令牌数量的更新时间\n" +
                    "local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or now)\n" +
                    "-- 漏桶在当前时间范围内已流出的令牌数\n" +
                    "local leaks = math.floor((now - lastUpdate) / 1000 * rate)\n" +
                    "-- 重新计算当前漏桶内的令牌数量 math.min(capacity, currentTokens + deltaTokens)\n" +
                    "currentTokens = math.max(currentTokens - leaks + requestedTokens, 0)\n" +
                    "-- 是否允许通过,默认不允许\n" +
                    "local isAllow = 0\n" +
                    "if currentTokens <= capacity then\n" +
                    "    -- 当前令牌数量还能放进去\n" +
                    "    isAllow = 1\n" +
                    "    redis.call('HSET', key, 'tokens', currentTokens)\n" +
                    "    redis.call('HSET', key, 'last_update', now)\n" +
                    "    redis.call('EXPIRE', key, expire);\n" +
                    "end\n" +
                    "return {isAllow, currentTokens}";


    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            int rate = 1;       // 速率(每秒恒定流出的token数量)
            int capacity = 10;  // 桶容量(可存放token数量)
            int everyTime = 1;  // 每次进来的token数量
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "LeakyBucketExample", String.valueOf(capacity), String.valueOf(rate), String.valueOf(now), String.valueOf(everyTime));
                List<Object> resutl = (List)o;
                if (Long.valueOf(1).equals(resutl.get(0))){
                    System.out.println(i + "当前桶内令牌数:" + resutl.get(1) + "==============================放行");
                }else {
                    System.out.println(i + "当前桶内令牌数:" + resutl.get(1) + "=拦截=============================");
                }
            }
        }
    }

}

算法对比

算法 工作机制 优点 缺点 使用场景
固定窗口 固定时间窗口内计数 实现简单,快速判断 窗口边界可能导致流量突发(边界效应) 简单的 API 限流,低要求的场景
滑动窗口 滑动时间窗口内计数 更精确地控制流量,减少流量突发 实现较复杂,较高的性能开销 动态限流场景,减少流量突增,如 API 网关
令牌桶 令牌以固定速率生成,请求消耗令牌 支持流量突发,且易于实现和理解 如果高峰流量持续时间过长,会导致后续请求被拒绝 适合支持突发流量的场景,如限速下载、API 限流
漏桶 固定速率处理请求,严格控制输出流量 严格控制流量,平滑输出 不允许流量突发 严格控制请求速率,如网络流量控制,负载均衡等

总结

  • 固定窗口简单易用,适合对流量要求不高的场景。
  • 滑动窗口平滑控制流量,适合对流量突发有一定需求但又希望平稳控制的场景。
  • 令牌桶允许突发流量,适合需要高效处理短时流量高峰的应用。
  • 漏桶严格控制请求速率,适合对平稳处理请求要求很高的场景。
相关推荐
希忘auto14 小时前
详解Redis的常用命令
redis·1024程序员节
yaosheng_VALVE1 天前
探究全金属硬密封蝶阀的奥秘-耀圣控制
运维·eclipse·自动化·pyqt·1024程序员节
dami_king1 天前
SSH特性|组成|SSH是什么?
运维·ssh·1024程序员节
一个通信老学姐6 天前
专业125+总分400+南京理工大学818考研经验南理工电子信息与通信工程,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节
sheng12345678rui6 天前
mfc140.dll文件缺失的修复方法分享,全面分析mfc140.dll的几种解决方法
游戏·电脑·dll文件·dll修复工具·1024程序员节
huipeng9267 天前
第十章 类和对象(二)
java·开发语言·学习·1024程序员节
earthzhang20217 天前
《深入浅出HTTPS》读书笔记(19):密钥
开发语言·网络协议·算法·https·1024程序员节
爱吃生蚝的于勒8 天前
计算机基础 原码反码补码问题
经验分享·笔记·计算机网络·其他·1024程序员节
earthzhang20218 天前
《深入浅出HTTPS》读书笔记(20):口令和PEB算法
开发语言·网络协议·算法·https·1024程序员节
一个通信老学姐8 天前
专业140+总分410+浙江大学842信号系统与数字电路考研经验浙大电子信息与通信工程,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节