方案设计思路
- 滑动窗口算法优化:采用滑动时间窗口算法替代固定窗口算法。固定窗口在窗口切换瞬间可能导致限流失效,滑动窗口通过将时间窗口划分为多个小格子,随着时间流逝,窗口平滑移动,能更精确地进行限流控制。
- Lua脚本实现原子性:使用Lua脚本来保证限流操作的原子性。Redis执行Lua脚本是原子性的,这样可以避免多线程或多进程环境下的竞态条件。
涉及的数据结构与操作命令
- 数据结构:
- 哈希表(Hash):用于存储每个限流维度(如IP地址、用户ID等)的滑动窗口统计信息。哈希表的key为限流维度标识,value是一个有序集合(Sorted Set),有序集合的member为时间窗口的小格子标识(可以是时间戳),score为该小格子内的请求计数。
- 操作命令:
- ZADD:用于向有序集合中添加成员。在滑动窗口中,每次有请求到来,使用ZADD将当前时间窗口小格子(时间戳)添加到对应的有序集合中,并将该小格子的请求计数加1(若小格子已存在,通过Lua脚本更新计数)。
- ZRANGEBYSCORE:用于获取滑动窗口内的请求计数总和。通过设置合适的score范围(对应滑动窗口的时间范围),获取窗口内的所有小格子及其计数,然后在Lua脚本中累加得到总请求数。
- EVAL:用于执行Lua脚本,保证限流操作的原子性。
Lua脚本示例
-- 获取当前时间戳
local current_time = tonumber(ARGV[1])
-- 限流窗口大小(秒)
local window_size = tonumber(ARGV[2])
-- 限流阈值
local limit = tonumber(ARGV[3])
-- 限流维度标识
local key = KEYS[1]
-- 添加当前时间窗口小格子(时间戳),计数为1
redis.call('ZADD', key, current_time, current_time)
-- 移除过期的时间窗口小格子
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)
-- 获取滑动窗口内的请求计数总和
local count = redis.call('ZCARD', key)
if count > limit then
-- 超出限流阈值,返回0表示拒绝请求
return 0
else
-- 未超出限流阈值,返回1表示允许请求
return 1
end
优点
- 精确限流:滑动窗口算法能更精确地控制请求流量,避免固定窗口在窗口切换瞬间的限流漏洞,提高限流的准确性。
- 原子性保障:通过Lua脚本在Redis中执行限流逻辑,确保了限流操作的原子性,防止并发环境下的不一致问题。
- 高性能:Redis本身是单线程模型,Lua脚本执行也是原子性的,在高并发场景下能有效减少锁竞争,提升处理效率。
缺点
- 复杂度增加:相比于固定窗口限流,滑动窗口算法和Lua脚本的实现相对复杂,增加了开发和维护的难度。
- 内存消耗:每个限流维度需要使用哈希表和有序集合来存储滑动窗口信息,随着限流维度的增多和时间窗口的细化,内存消耗会相应增加。