【限流】基于springboot(拦截器) + redis(执行lua脚本)实现注解限流

实现了滑动窗口,固定窗口,令牌桶,漏桶四种限流算法,并且支持各种扩展和修改,源码简单易上手。
Giteehttps://gitee.com/sir-tree/rate-limiter-spring-boot-starter

一、令牌桶算法—入桶量限制

在客户端请求打过来的时候,会去桶里拿令牌,拿到就请求成功,拿不到不好意思,服务器拒绝服务。

关键点
  • 桶有多大?您定😊
  • 桶里的令牌一开始有多少?您定😊
  • 桶啥时候补充令牌?您定😊
  • 补充多少令牌?您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 看一下是否到补充桶的时候了,没到就到第2步,到了就补充完后再继续第2步
  2. 看一下桶里有没有令牌,有就拿走,没有就不好意思,服务器拒绝服务。
扩展:加个黑名单

请求打过来

  1. 看一下在不在黑名单,在就拒绝服务,不在,就进行第2步
  2. 看一下是否到补充桶的时候了,没到就到第2步,到了就补充完后再继续第3步
  3. 看一下桶里有没有令牌,有就拿走,没有就不好意思,加黑名单
Lua实现
-- 参数初始化
local key = KEYS[1]
local maxCapacity = tonumber(ARGV[1]) -- <= 桶有多大/桶里的令牌一开始有多少 我写成一样的了
local fill = tonumber(ARGV[2]) -- <= 补充多少令牌
local now = tonumber(ARGV[3])
local duration = tonumber(ARGV[4]) -- <= 桶啥时候补充令牌
local blackKey = key..'-black'
local blackDuration = tonumber(ARGV[5]) -- <= 黑名单多久后放开
local expire = math.ceil(now / fill) * duration

-- 黑名单
local blackValue = redis.call('get', blackKey)
if blackValue then
    return -1;
end

-- 当前值
local kValue = redis.call('get', key)

-- 第一次
if not kValue then
    -- 当前消耗#刷新时间#容量
    kValue = table.concat({'0', tostring(now + duration), tostring(maxCapacity)}, '#')
end

-- 解析
local parts = {}
for part in string.gmatch(kValue, "[^#]+") do
    table.insert(parts, part)
end

local value = tonumber(parts[1])
local refresh = tonumber(parts[2])
local capacity = tonumber(parts[3])
local fillTimes = math.floor((now - refresh) / duration) + 1

-- 刷新桶令牌
if now >= refresh then
    value = 0
    -- 补充
    capacity = capacity + fill * fillTimes
    -- 最多补满
    if capacity > maxCapacity then
        capacity = maxCapacity
    end
    -- 刷新时间
    refresh = now + duration
end

-- 拿完
if value == capacity then
    -- 加入黑名单
    if blackDuration > 0 then
        redis.call('set', blackKey, '_', 'PX', blackDuration)
        redis.call('del', key)
    end
    return -1
end

-- 期间
value = value + 1
kValue = table.concat({tostring(value), tostring(refresh), tostring(capacity)}, '#')
redis.call('set', key, kValue, 'PX', expire)

return capacity - value

二、 漏桶算法—出桶量限制

在客户端请求打过来的时候,会去桶里放令牌(有唯一标识),放成功后尝试把自己放的令牌拿走,拿不走就不断的尝试拿,直到拿走;但如果放失败了,不好意思,服务器拒绝服务。

关键点
  • 桶有多大?您定😊
  • 哪些令牌可以被自己拿走?您定不了了😎,我定,这里有两个方案,公平的方案每个请求只能拿自己放的令牌,先放进来的令牌可以先被拿走不公平的方案是拿谁放的都可以,大家一起抢,谁抢到算谁的
  • 多长时间内最多可以拿走多少个令牌?‘多长时间’您定😊,‘最多可拿走’您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 放令牌成功就执行第2步,但桶满了放不了了,不好意思,服务器拒绝服务
  2. 不断尝试拿令牌(两种方案实现) –不断尝试拿!!不断尝试拿!!!不断尝试拿!!!
实现
-- 参数初始化
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- <= 桶有多大
local pass = tonumber(ARGV[2]) -- <= 多长时间内最多可以拿走多少个令牌 -- 最多可以拿走多少个令牌
local duration = tonumber(ARGV[3]) -- <= 多长时间内最多可以拿走多少个令牌 -- 多长时间内
local fair = tonumber(ARGV[4]) -- <= 公平/不公平方案
local queueId = tonumber(ARGV[5]) -- <= 哪些令牌可以被自己拿走
local exceedQueueId = -(capacity + 1)
local legacyPassKey = key..'-legacyPass'
local queueKey = key..'-queue'

-- redis初始化
local qMembers = redis.call('lRange', queueKey, 0, -1)
local lpValue = redis.call('get', legacyPassKey)
local lpExpire = redis.call('pTtl', legacyPassKey)

local function min(q)
    if #q == 0 then
        return 0
    end
    local mv = tonumber(q[1])
    local tmp = mv
    for i = 2, #q do
        tmp = tonumber(q[i])
        if tmp < mv then
            mv = tmp
        end
    end
    return mv
end

local function addQueue(qId)
    if qId ~= exceedQueueId then
        return qId
    end
    if #qMembers == capacity then
        return exceedQueueId
    end
    qId = min(qMembers) - 1
    redis.call('rPush', queueKey, qId)
    table.insert(qMembers, tostring(qId))
    return qId
end

-- 1 删除成功,0删除失败
local function delQueue()
    -- >= 队列不为空,队列有元素
    if #qMembers > 0 then
        -- 公平
        if fair > 0 then
            -- 第一个不是当前请求
            if tonumber(qMembers[1]) ~= queueId then
                return 0
            else
                redis.call('lPop', queueKey)
                table.remove(qMembers, 1)
            end
        else -- 不公平
            redis.call('lRem', queueKey, 1, queueId)
            for i = #qMembers, 1, -1 do
                if tostring(qMembers[i]) == queueId then
                    table.remove(qMembers, i)
                end
            end
        end
    end
    -- 队列无元素,或队列第一个是
    return 1
end

-- 第一次/到刷新时间-刷新pass
if lpExpire <= 0 then
    local update = delQueue()
    if update == 1 then
        pass = pass - 1
    else
        queueId = addQueue(queueId)
    end
    redis.call('set', legacyPassKey, pass, 'PX', duration)

    if update == 1 then
        return pass
    end
    return queueId
end

lpValue = tonumber(lpValue)
-- 如果lpValue >= 0 表示当前不需要等待
if lpValue - 1 >= 0 then
    if delQueue() == 0 then
        return queueId
    end
    lpValue = lpValue - 1
    redis.call('set', legacyPassKey, lpValue, 'PX', lpExpire)
    return lpValue
else
    return addQueue(queueId)
end

三、 固定窗口算法

时间窗口,随时间变化,比如1秒10个,即时间窗口大小为1秒,窗口内最多允许10个请求,随时间变化,如0 ~ 1秒有一个时间窗口,1 ~ 2秒内又是一个时间窗口 … 且每个窗口内最多有10个请求,超过的就放弃。

关键点
  • 时间窗口内允许最多多少个请求?您定😊
  • 时间窗口大小是多大?您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 看一下还剩下多少个位置,不剩就服务器拒绝服务,否则成功
Lua实现
local key = KEYS[1]
local limit = tonumber(ARGV[1]) -- <= 时间窗口内允许最多多少个请求
local window = tonumber(ARGV[2]) -- <= 时间窗口大小是多大

local expire = redis.call('pTtl', key)
local kValue = redis.call('get', key)

if expire <= 0 then
    redis.call('set', key, 1, 'PX', window)
    return 1 -- 通过
end

kValue = tonumber(kValue)
if kValue == limit then
    return 0 -- 限流
else
    redis.call('set', key, kValue + 1, 'PX', expire)
    return 1 -- 通过
end

四、滑动窗口

和固定窗口类似,只是这个窗口是动的,怎么动?每次请求过来的时候,就以当前请求为窗口的右端点,而不是固定窗口那样固定。

Lua实现
local key = KEYS[1]
local limit = tonumber(ARGV[1]) -- <= 时间窗口内允许最多多少个请求
local window = tonumber(ARGV[2]) -- <= 时间窗口大小是多大
local now = tonumber(ARGV[3]) -- <= 就以当前请求为窗口的右端点

local start = now - window
redis.call('zRemRangeByScore', key, 0, start) -- 移除上一个窗口期之前的数据
local size = redis.call('zCard', key)

if size == limit then
    return 0 -- 限制
else
    redis.call('zAdd', key, now, now)
    redis.call('pExpire', key, window + 1000) -- 窗口期 + 1秒后过期
    return 1 -- 通过
end

讨论

  • 固定窗口问题:比如窗口是1秒最多10个,当流量在第一个窗口最后100毫秒满10个,且在第二个窗口前100毫秒满10个时,这200毫秒可以看做1秒内,也就是一个窗口,这就有问题(流量激增)…
  • 令牌桶问题:不能平稳的处理请求…
  • 滑动窗口:请求打满后,必须要延迟等到下一个窗口…
  • 漏桶:突发流量会有一堆被抛弃的…
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值