redis+lua实现限流

365 阅读5分钟

Redis + Lua 实现限流的核心优势在于 Lua 脚本的执行是原子性的
常见的限流算法,这里讲三种,固定窗限流,滑动窗限流和令牌桶限流
使用的是redis的eval命令
EVAL script numkeys [key [key ...]] [arg [arg ...]]
script ---脚本
numkeys --指定键的数量,前几个是键
[key [key ...]] 这一截就是键
[arg [arg ...]] 这一截就是参数

一、固定窗限流

1.思路

先来考虑一下思路,采用redis的eval命令执行lua做:实现一个1秒只能通过5个请求;
需要一个key存储一个int值,用存的值和5做比较,小于5能通过,值加1,否则不能通过,因为每1秒5个请求,所以key的有效时长设置成1秒,这样下一秒的请求过来,前面的key实失效了,这样又能继续通过
这里每次加1使用redis的自增命令即可

每次请求过来key的有效时间刷不刷新呢?
不能,假设当0.8秒来了一个请求,正好是第5个请求,那么现在满了,第6个请求是1.2秒过来的,现在已经是下一秒了,按理说应该通过, 但是由于前一秒的key还存在,且值已经等于5了,所以通过不了,因此每秒第一次设置有效时长就可以了

2.编写lua脚本

local key = KEYS[1]              --这个是redis键 
local time = ARGV[1]             --时间 
local limit = ARGV[2]            --允许通过的数量 
local value = redis.call("incr",key)  --执行incr自增命令,返回key对应的value 
if(value == 1)    --值为1,说明第一次设置一下有效时长
then 
  redis.call("expire",key,time)
end 
return value >= limit and 1 or 0  --判断1就是能通过,0就是不能通过

执行EVAL script 1 key123 1 5

二、滑动窗限流

所谓滑动窗采用的是redis的有序列表
使用到的命令: ZREMRANGEBYRANK key start stop
删除有序列表索引从start 到stop (包含start,stop) 的值
添加命令:ZADD key score member
该命令会使用score值来排序

1.思路

还是假设:每秒只能通过5个请求,我们加入列表的命令的score可以使用时间戳,这样我们每次请求过来就干掉小于当前时间戳的元素,用剩下的元素个数和5做比较即可,

2.编写lua脚本

local key = KEYS[1]           -- redis的键,存列表的 
local capacity = ARGV[1]      -- 通过的数量
local unit_time = ARGV[2]     -- 单位时间 
local uuid = ARGV[3]          -- uuid 
local current_temp = redis.call("TIME")[1] -- 当前时间戳 

-- 删除单位时间之前的列表成员 
redis.call("ZREMRANGEBYRANK",key,0,current_temp - unit_time * 1000) 

-- 获取列表成员数量 
local num = redis.call("ZCARD",key) 

-- 比较容量和列表成员大小 
local differ = capacity > tonumber(num) 
-- 容量大于列表成员数量,则添加成员,并设置过期时间 
if(differ) 
then 
  redis.call("ZADD", key,current_temp,uuid)
  redis.call("expire", key,tonumber(unit_time)); 
end 
return differ

执行EVAL script 1 key123 5 1 uuid
这样就达到了效果

三、令牌桶算法

1.思路

令牌桶稍微复杂点,我们通过令牌是否还有剩余来判断的
假设:每2秒8个请求
(1).首先需要一个key来存令牌数量,嗯就是桶,判断桶的数量是否大于1,大于有令牌,可以拿到令牌(请求通过),没有那就不行。这个key的有效时间为单位时间,每次执行都刷新有效时长,假设0.5秒来了三个请求那么桶还有5个令牌,1秒又来了3个请求,桶里还有2个令牌,2秒又来了2个请求,ok现在桶里没有令牌了,但是由于时间每次都会刷新现在key还是存在的,那么2.5秒又来一个请求,这个请求应该能通过,因为令牌的生产速度是每秒4个令牌 (2)还需要一个key存上次的刷新时间,来计算桶里的生产速度(当前时间戳-上次刷新时间戳)* 生产速度

2.编写lua脚本

local tokens_key = KEYS[1]      -- 存桶数量的key
local timestamp_key = KEYS[2]   -- 存上次刷新时间的key
local capacity = tonumber(ARGV[1])  -- 桶容量
local time = tonumber(ARGV[2])      -- key的有效时间 
local now = redis.call("TIME")[1]   -- 当前时间戳 
local speed = capacity/time         -- 将桶装满的时间(容量/有效时间) 

-- 桶剩余令牌数,不存在就设置为capacity 
local remain_capacity = tonumber(redis.call('get',tokens_key)) or capacity 

-- 上次刷新时间,不存在就设置为 0 
local last_refresh_time = tonumber(redis.call('get',timestamp_key)) or 0

--计算两次时间差 
local delta = math.max(0, now - last_refresh_time)

--计算当前令牌数量,不能大于容量
local num = min(capacity,remain_capacity + delta * speed ) 

--判断如果大于1,则将cur_num 设置成 当前数量-1,否则不变
local cur_num = num
if(num >= 1) 
then 
  cur_num = num - 1 
end 

--设置tokens_key的值为 cur_num,有效时间为time 
--设置timestamp_key的值为now,有效时间为time 
redis.call("setex", tokens_key, time, cur_num) 
redis.call("setex", timestamp_key, time, now)

return num >= 1 and 1 or 0

执行EVAL script 1 key123 time123 8 2

如果将令牌桶算法的有效时长每次请求不刷新,那么就不需要第二个key,会发现和固定窗算法很像,为什么会出现两种实现方式,在系统中应用时,作用区别在哪里?