限流算法

固定窗口(Fixed-window)

固定窗口算法将时间分割成固定长度的窗口,在每个窗口中计算请求次数。如果请求次数超过了设定的阈值,后续请求将被拒绝。该算法易于实现,并且针对DDOS攻击有效,但可能会限制合法用户。

  • 适用场景:限制合法用户,如购买流量包或API调用次数包
  • 问题:在窗口切换的瞬间可能会有两倍于限制的请求被允许,即“窗口切换”问题。
  • Redis实现
  • 每个时间窗作为一个key-value计数器,使用当前时间作为key的一部分,并设置过期时间
  • 每次请求时用INCR加一,并检查是否超过限制
local key = KEYS[1]
local requests = tonumber(redis.call('GET', key) or '-1')
local max_requests = tonumber(ARGV[1])
local expiry = tonumber(ARGV[2])

if (requests == -1) or (requests < max_requests) then
  redis.call('INCR', key)
  redis.call('EXPIRE', key, expiry)
  return false
else
  return true
end

滑动窗口(Sliding-window)

滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。它是固定窗口的一种改进,但从根本上并没有真正解决固定窗口算法的临界突发流量问题。

  • 适用场景:限制合法用户,如购买流量包或API调用次数包,适用于需要更精细控制请求分布的场景
  • 问题:依然存在单倍峰值问题,导致请求被拒绝。且实现相对复杂,需要更多的存储来追踪滑动窗口。
  • Redis实现
  • 使用一个sorted set作为时间窗,每次请求时将当前时间作为元素放入set中
  • 移除窗口外的元素,再查看set的大小是否超过限制
local current_time = redis.call('TIME')
local trim_time = tonumber(current_time[1]) - tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, trim_time)
local request_count = redis.call('ZCARD',KEYS[1])

if request_count < tonumber(ARGV[3]) then
    redis.call('ZADD', KEYS[1], current_time[1])
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return true
end
return false

漏桶(Leaky Bucket)

想象有一个木桶,桶的容量是固定的。当有请求到来时先放到木桶中,处理请求的worker以固定的速度从木桶中取出请求进行相应。如果木桶已经满了,直接返回请求频率超限的错误码或者页面。

  • 适用场景:以严格速率保护系统资源
  • 问题:在高流量下,令牌桶可能很快被耗尽,导致请求被拒绝,也导致获得令牌的请求产生高延迟。
  • Redis实现
  • TBD

令牌桶(Token Bucket)

想象有一个木桶,桶的容量是固定的。以恒定的速度往木桶里加入令牌,木桶满了则不再加入令牌。服务收到请求时尝试从木桶中取出一个令牌,如果能够得到令牌则继续执行后续的业务逻辑。如果没有得到令牌,直接返回访问频率超限的错误码或页面等,不继续执行后续的业务逻辑。令牌桶兼备窗口和漏桶的功能,即既可以恒定速率限流的同时又可以应对一定的突发流量。

  • 适用场景:应对突发流量峰值同时保护系统资源
  • 问题:在高流量下,令牌桶可能很快被耗尽,导致请求被拒绝。
  • Redis实现
  • TBD

参考信息

  • https://developer.redis.com/develop/java/spring/rate-limiting/fixed-window/reactive-lua/
  • https://developer.redis.com/develop/dotnet/aspnetcore/rate-limiting/sliding-window/
  • https://www.cnblogs.com/ciel717/p/16180017.html