Redis滑动窗口实现限流算法

wen java案例 1

本文目录导读:

Redis滑动窗口实现限流算法

  1. 基于ZSET的滑动窗口实现
  2. 基于LIST的实现
  3. 基于RedisTimeSeries模块
  4. 多维度限流实现
  5. 优化和注意事项
  6. 完整的使用示例

我来详细介绍Redis实现滑动窗口限流算法的多种方法:

基于ZSET的滑动窗口实现

这是最常见的实现方式,利用有序集合的时间戳特性。

核心实现

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String prefix = "rate:limiter:";
    public SlidingWindowRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }
    /**
     * 滑动窗口限流
     * @param key 限流key
     * @param windowSize 窗口大小(秒)
     * @param maxRequests 最大请求数
     * @return 是否允许通过
     */
    public boolean allowRequest(String key, int windowSize, int maxRequests) {
        String windowKey = prefix + key;
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - windowSize * 1000L;
        // 使用Pipeline批量操作
        Pipeline pipeline = jedis.pipelined();
        // 1. 移除窗口外的数据
        pipeline.zremrangeByScore(windowKey, 0, windowStart);
        // 2. 添加当前请求
        pipeline.zadd(windowKey, currentTime, String.valueOf(currentTime));
        // 3. 统计窗口内的请求数
        Response<Long> count = pipeline.zcard(windowKey);
        // 4. 设置过期时间
        pipeline.expire(windowKey, windowSize + 1);
        pipeline.sync();
        // 检查是否超过限制
        return count.get() <= maxRequests;
    }
}

Lua脚本实现(原子性操作)

-- 滑动窗口限流Lua脚本
-- KEYS[1]: 限流key
-- ARGV[1]: 窗口大小(秒)
-- ARGV[2]: 最大请求数
local windowKey = KEYS[1]
local windowSize = tonumber(ARGV[1])
local maxRequests = tonumber(ARGV[2])
local currentTime = redis.call('TIME')[1] * 1000  -- 获取Redis时间,转为毫秒
local windowStart = currentTime - windowSize * 1000
-- 1. 移除过期数据
redis.call('ZREMRANGEBYSCORE', windowKey, 0, windowStart)
-- 2. 获取当前请求数
local currentCount = redis.call('ZCARD', windowKey)
-- 3. 判断是否限流
if currentCount >= maxRequests then
    return 0  -- 限流
end
-- 4. 允许请求,添加记录
redis.call('ZADD', windowKey, currentTime, currentTime)
redis.call('EXPIRE', windowKey, windowSize + 1)
return 1  -- 通过

Java调用Lua脚本:

public class LuaSlidingWindowRateLimiter {
    private Jedis jedis;
    private String luaScript;
    public LuaSlidingWindowRateLimiter(Jedis jedis) {
        this.jedis = jedis;
        // 加载Lua脚本
        this.luaScript = loadLuaScript();
    }
    public boolean allowRequest(String key, int windowSize, int maxRequests) {
        String scriptSHA = jedis.scriptLoad(luaScript);
        Long result = (Long) jedis.evalsha(
            scriptSHA, 
            1,  // 1个key
            key, 
            String.valueOf(windowSize),
            String.valueOf(maxRequests)
        );
        return result == 1L;
    }
}

基于LIST的实现

使用List存储请求时间戳,实现更轻量级的滑动窗口。

public class ListBasedRateLimiter {
    private Jedis jedis;
    public boolean allowRequest(String key, int windowSize, int maxRequests) {
        String windowKey = "rate:list:" + key;
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - windowSize * 1000L;
        Pipeline pipeline = jedis.pipelined();
        // 移除过期请求
        while (true) {
            String oldest = jedis.lindex(windowKey, -1);
            if (oldest == null || Long.parseLong(oldest) > windowStart) {
                break;
            }
            jedis.rpop(windowKey);
        }
        // 检查当前数量
        long currentCount = jedis.llen(windowKey);
        if (currentCount >= maxRequests) {
            return false;
        }
        // 添加新请求(头部)
        jedis.lpush(windowKey, String.valueOf(currentTime));
        jedis.expire(windowKey, windowSize + 1);
        return true;
    }
}

基于RedisTimeSeries模块

如果Redis安装了TimeSeries模块,可以使用更专业的时间序列功能。

// 需要RedisTimeSeries模块支持
public class TimeSeriesRateLimiter {
    private Jedis jedis;
    public void createRule(String key, int windowSize, int maxRequests) {
        // 创建时间序列
        jedis.sendCommand(
            Protocol.Command.valueOf("TS.CREATE"),
            "ts:" + key,
            "RETENTION", String.valueOf(windowSize * 1000),  // 保留时间
            "LABELS", "type", "rate_limit"
        );
    }
    public boolean allowRequest(String key, int maxRequests) {
        long currentTime = System.currentTimeMillis() * 1000;  // 微秒
        // 添加时间戳
        jedis.sendCommand(
            Protocol.Command.valueOf("TS.ADD"),
            "ts:" + key,
            String.valueOf(currentTime),
            "1"  // 值
        );
        // 查询窗口内的请求数
        long windowStart = currentTime - windowSize * 1000 * 1000;  // 微秒
        Object result = jedis.sendCommand(
            Protocol.Command.valueOf("TS.RANGE"),
            "ts:" + key,
            String.valueOf(windowStart),
            String.valueOf(currentTime)
        );
        // 解析结果,统计请求数
        return parseCount(result) <= maxRequests;
    }
}

多维度限流实现

支持不同粒度的限流(秒级、分钟级、小时级)。

public class MultiDimensionRateLimiter {
    private Jedis jedis;
    public boolean allowRequest(String userId, String apiPath) {
        // 多维度限流规则
        Map<String, Integer> rules = new HashMap<>();
        rules.put("second", 10);   // 每秒10次
        rules.put("minute", 100);  // 每分钟100次
        rules.put("hour", 1000);   // 每小时1000次
        for (Map.Entry<String, Integer> rule : rules.entrySet()) {
            String windowKey = getWindowKey(userId, apiPath, rule.getKey());
            int windowSize = getWindowSize(rule.getKey());
            if (!checkWindow(windowKey, windowSize, rule.getValue())) {
                return false;  // 任意维度超限则拒绝
            }
        }
        return true;
    }
    private String getWindowKey(String userId, String apiPath, String dimension) {
        return String.format("rate:%s:%s:%s", userId, apiPath, dimension);
    }
    private int getWindowSize(String dimension) {
        switch (dimension) {
            case "second": return 1;
            case "minute": return 60;
            case "hour": return 3600;
            default: return 60;
        }
    }
}

优化和注意事项

内存优化版本

public class OptimizedRateLimiter {
    // 使用HyperLogLog减少内存占用
    public boolean allowRequestHLL(String key, int windowSize, int maxRequests) {
        String hllKey = "rate:hll:" + key + ":" + getWindowSlot();
        jedis.pfadd(hllKey, String.valueOf(System.nanoTime()));
        long count = jedis.pfcount(hllKey);
        return count <= maxRequests;
    }
    private String getWindowSlot() {
        long currentTime = System.currentTimeMillis();
        // 将时间划分为固定窗口
        return String.valueOf(currentTime / 10000);  // 10秒一个槽
    }
}

分布式环境下的实现

public class DistributedRateLimiter {
    private RedissonClient redisson;
    public boolean allowRequest(String key, int limit, int windowSize) {
        RRateLimiter rateLimiter = redisson.getRateLimiter(key);
        // 设置速率:limit次/windowSize秒
        rateLimiter.trySetRate(
            RateType.OVERALL, 
            limit, 
            windowSize, 
            RateIntervalUnit.SECONDS
        );
        return rateLimiter.tryAcquire();
    }
}

完整的使用示例

public class RateLimiterDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(jedis);
        // 模拟请求
        String userId = "user:123";
        int windowSize = 60;  // 60秒窗口
        int maxRequests = 10; // 最多10次请求
        for (int i = 0; i < 20; i++) {
            boolean allowed = limiter.allowRequest(userId, windowSize, maxRequests);
            System.out.println("Request " + (i + 1) + ": " + (allowed ? "ALLOWED" : "REJECTED"));
            try {
                Thread.sleep(1000);  // 间隔1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        jedis.close();
    }
}

滑动窗口限流的核心是:

  1. 数据结构:ZSET是最常用的,支持按分数(时间戳)范围查询和删除
  2. 原子性:使用Lua脚本确保操作的原子性
  3. 内存管理:及时清理过期数据,设置合理的TTL
  4. 性能优化:使用Pipeline减少网络开销

选择哪种实现取决于你的具体需求:

  • ZSET方案:最灵活,适合大多数场景
  • List方案:简单轻量,适合请求量不大的场景
  • TimeSeries方案:专业的时间序列处理,需要额外模块
  • 多维度方案:需要复杂的限流策略时使用

抱歉,评论功能暂时关闭!