轻量级分布式接口限流器
已发布github:https://github.com/z-meng/CurrentRateLimiter
相对于令牌桶,该方案思路、设计都简单易上手
算法设计:基于redis + lua
优点:简单,具有基于时间的滑动窗口功能
缺点:当前没有阻塞策略
整体思路,
1、每次请求操作redis链表,查询存在不存在,不存在则新增基于条件(用户+接口/IP+接口)的链表,value为当前接口请求的时间戳,
2、当表存在时候,判断长度是否小于限流阈值,不小于则从左边插入当前时间戳
3、否则的话就从右边弹出一个数据,判断该数据和当前接口请求的时间戳的差值,是否大于 1S,大于,说明处于阈值的时间戳和当前请求的时间差超过了1S,那么起码可以请求一次,满足当前请求,把当前请求插入,并截取链表长度,到阈值长度(其实没必要截取了,从右边弹出 + lua原子性 + redis单线程,此时已经保证了链表中最多只有=阈值的数据,因为每当该数据等于阈值时候,就把最早的数据弹出了)
local key = KEYS[1]
local nowMill = tonumber(ARGV[1]) --当前时间戳毫秒
local limit = tonumber(ARGV[2]) --限流次数
local interval = tonumber(ARGV[3]) --限流间隔 单位是毫秒
local startIndex = 0
local endIndex = limit -1
--1、判断是否存在key
local isExists = redis.call("EXISTS",key);
if not isExists then
redis.call("LPUSH",key,nowMill) --不存在 插入
else
local size = tonumber(redis.call("LLEN",key)) --获取列表长度
if size >= limit then
local lastMill = tonumber(redis.call("RPOP",key)) --获取最后一个数据
local diffMill = nowMill - lastMill
if diffMill <= interval then
redis.call("LPUSH",key,nowMill) --从头部插入数据
redis.call("LTRIM",key,startIndex,endIndex) --剪切保留最新的limit条记录
return false
end
end
redis.call("LPUSH",key,nowMill) --从头部插入数据
end
return true
lua基于springboot的配置
@Service
public class LuaConfig {
@Bean
public DefaultRedisScript<Boolean> currentLimitScript() {
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/currentlimit.lua")));
redisScript.setResultType(Boolean.class);
return redisScript;
}
}
java的service方法
String key = PREFIX + userId;
boolean flag = redisTemplate.execute(currentLimitScript, Collections.singletonList(key), Long.toString(System.currentTimeMillis()),
Integer.toString(limit), Long.toString(interval));
if(!flag){
log.error("接口调用过于频繁!");
}
return flag;
到此就结束了,每次接口调用的入口就调用该方法,就能达到分布式限流的功能,lua原子性、redis单线程、也可以写一个自定义注解,这里比较懒,没有实现