Redis+Lua实战:分布式滑动窗口限流算法全解析
目录
- 引言:限流的意义与应用场景
限流算法概览
- 固定窗口限流
- 滑动窗口限流
- 漏桶与令牌桶
分布式滑动窗口限流的原理
- 滑动窗口算法思路
- 分布式实现挑战
- Redis与Lua结合优势
Redis+Lua实现分布式滑动窗口限流
- 数据结构设计
- Lua脚本详解
- Redis调用方式
完整代码示例
- Python示例
- Node.js示例
- 工作流程图解
- 性能优化与注意事项
- 总结与实践建议
1. 引言:限流的意义与应用场景
在高并发场景下,服务端需要对请求进行限流,以防止系统过载。典型应用场景包括:
- API接口防刷
- 秒杀活动限流
- 微服务调用流量控制
分布式系统中,单点限流容易成为瓶颈,因此采用Redis+Lua实现的分布式滑动窗口限流,成为高性能、高可用的方案。
2. 限流算法概览
2.1 固定窗口限流(Fixed Window)
- 按固定时间窗口统计请求数量
- 简单,但存在“临界点超额”的问题
窗口长度:1秒
请求限制:5次
时间段:[0s-1s]
请求次数统计:超过5次则拒绝
2.2 滑动窗口限流(Sliding Window)
- 按时间连续滑动,统计最近一段时间的请求
- 精度高,平滑处理请求峰值
实现方式:
- 精确计数(存储请求时间戳)
- Redis Sorted Set(ZSET)存储请求时间戳
2.3 漏桶与令牌桶
- 漏桶:固定出水速度,适合平滑处理请求
- 令牌桶:以固定速率生成令牌,灵活控制突发请求
本文重点讲解滑动窗口算法。
3. 分布式滑动窗口限流的原理
3.1 滑动窗口算法思路
滑动窗口算法核心:
- 记录请求时间戳
每次请求:
- 删除超出窗口的旧请求
- 判断当前窗口内请求数量是否超限
- 超限则拒绝,否则允许
公式:
允许请求数量 = COUNT(时间戳 > 当前时间 - 窗口长度)
3.2 分布式实现挑战
- 多实例并发请求
- 原子性操作要求:检查+增加
- 高并发下操作Redis性能问题
3.3 Redis+Lua结合优势
- Lua脚本在Redis端执行,保证原子性
- 减少网络往返次数,提高性能
4. Redis+Lua实现分布式滑动窗口限流
4.1 数据结构设计
使用 Redis Sorted Set (ZSET):
- key:接口标识 + 用户ID
- score:请求时间戳(毫秒)
- value:唯一标识(可用时间戳+随机数)
4.2 Lua脚本详解
-- KEYS[1] : 限流key
-- ARGV[1] : 当前时间戳 (毫秒)
-- ARGV[2] : 窗口长度 (毫秒)
-- ARGV[3] : 最大请求数
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 0 -- 限流
else
-- 添加新请求
redis.call('ZADD', key, now, now .. '-' .. math.random())
-- 设置过期时间
redis.call('PEXPIRE', key, window)
return 1 -- 允许
end
4.3 Redis调用方式
Python调用示例(使用redis-py
)
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
lua_script = """
-- Lua脚本内容同上
"""
def is_allowed(user_id, limit=5, window=1000):
key = f"rate_limit:{user_id}"
now = int(time.time() * 1000)
return r.eval(lua_script, 1, key, now, window, limit)
for i in range(10):
if is_allowed("user123"):
print(f"请求{i}: 允许")
else:
print(f"请求{i}: 限流")
Node.js调用示例(使用ioredis
)
const Redis = require('ioredis');
const redis = new Redis();
const luaScript = `
-- Lua脚本内容同上
`;
async function isAllowed(userId, limit=5, window=1000) {
const key = `rate_limit:${userId}`;
const now = Date.now();
const result = await redis.eval(luaScript, 1, key, now, window, limit);
return result === 1;
}
(async () => {
for (let i = 0; i < 10; i++) {
const allowed = await isAllowed('user123');
console.log(`请求${i}: ${allowed ? '允许' : '限流'}`);
}
})();
5. 工作流程图解
+---------------------+
| 用户请求到达服务端 |
+---------------------+
|
v
+---------------------+
| 执行Lua脚本(原子) |
| - 清理过期请求 |
| - 判断请求数 |
| - 添加请求记录 |
+---------------------+
|
+-----+-----+
| |
v v
允许请求 限流返回
- Lua脚本保证操作原子性
- Redis ZSET高效管理时间戳
6. 性能优化与注意事项
- 键过期设置:使用
PEXPIRE
防止ZSET无限增长 - ZSET最大长度:可结合
ZREMRANGEBYRANK
控制极端情况 - Lua脚本缓存:避免每次发送脚本,提高性能
- 分布式部署:所有实例共享同一个Redis节点/集群
7. 总结与实践建议
- 滑动窗口比固定窗口更平滑,适合高并发场景
- Redis+Lua实现保证原子性和性能
- 分布式系统可横向扩展,限流逻辑一致
实践建议:
- 精确控制请求速率,结合缓存和数据库保护后端
- 监控限流命中率,动态调整参数
- Lua脚本可扩展:按接口/用户/IP限流
评论已关闭