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脚本 ```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脚本 ```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脚本 ```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,会发现和固定窗算法很像,为什么会出现两种实现方式,在系统中应用时,作用区别在哪里?