本文目录导读:

我来详细介绍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();
}
}
滑动窗口限流的核心是:
- 数据结构:ZSET是最常用的,支持按分数(时间戳)范围查询和删除
- 原子性:使用Lua脚本确保操作的原子性
- 内存管理:及时清理过期数据,设置合理的TTL
- 性能优化:使用Pipeline减少网络开销
选择哪种实现取决于你的具体需求:
- ZSET方案:最灵活,适合大多数场景
- List方案:简单轻量,适合请求量不大的场景
- TimeSeries方案:专业的时间序列处理,需要额外模块
- 多维度方案:需要复杂的限流策略时使用