redis 如何实现限流的,但是大部分都有一个缺点,就是只能实现单一的限流,比如 1 分钟访问 1 次或者 60 分钟访问 10 次这种,
但是如果想一个接口两种规则都需要满足呢,项目又是分布式项目,应该如何解决,下面就介绍一下 redis 实现分布式多规则限流的方式。
- 如何一分钟只能发送一次验证码,一小时只能发送 10 次验证码等等多种规则的限流;
- 如何防止接口被恶意打击(短时间内大量请求);
- 如何限制接口规定时间内访问次数。
一:使用 string 结构记录固定时间段内某用户 ip 访问某接口的次数
- rediskey = prefix : classname : methodname
- redisvlue = 访问次数
拦截请求:
- 初次访问时设置 [rediskey] [redisvalue=1] [规定的过期时间];
- 获取 redisvalue 是否超过规定次数,超过则拦截,未超过则对 rediskey 进行加1。
规则是每分钟访问 1000 次
- 假设目前 rediskey => redisvalue 为 999;
- 目前大量请求进行到第一步( 获取 redis 请求次数 ),那么所有线程都获取到了值为999,进行判断都未超过限定次数则不拦截,导致实际次数超过 1000 次
- 解决办法: 保证方法执行原子性(加锁、lua)。
考虑在临界值进行访问

二:使用 zset 进行存储,解决临界值访问问题

三:实现多规则限流
①、先确定最终需要的效果(能实现多种限流规则+能实现防重复提交)
@ratelimiter(
rules = {
// 60秒内只能访问10次
@raterule(count = 10, time = 60, timeunit = timeunit.seconds),
// 120秒内只能访问20次
@raterule(count = 20, time = 120, timeunit = timeunit.seconds)
},
// 防重复提交 (5秒钟只能访问1次)
preventduplicate = true
)
②、注解编写
ratelimiter 注解
@target(elementtype.method)
@retention(retentionpolicy.runtime)
@inherited
public @interface ratelimiter {
/**
* 限流key
*/
string key() default rediskeyconstants.rate_limit_cache_prefix;
/**
* 限流类型 ( 默认 ip 模式 )
*/
limittypeenum limittype() default limittypeenum.ip;
/**
* 错误提示
*/
resultcode message() default resultcode.request_more_error;
/**
* 限流规则 (规则不可变,可多规则)
*/
raterule[] rules() default {};
/**
* 防重复提交值
*/
boolean preventduplicate() default false;
/**
* 防重复提交默认值
*/
raterule preventduplicaterule() default @raterule(count = 1, time = 5);
}
raterule 注解:
@target(elementtype.annotation_type)
@retention(retentionpolicy.runtime)
@inherited
public @interface raterule {
/**
* 限流次数
*/
long count() default 10;
/**
* 限流时间
*/
long time() default 60;
/**
* 限流时间单位
*/
timeunit timeunit() default timeunit.seconds;
}
③、拦截注解 ratelimiter
- 确定 redis 存储方式
rediskey = prefix : classname : methodname
redisscore = 时间戳
redisvalue = 任意分布式不重复的值即可 - 编写生成 rediskey 的方法
public string getcombinekey(ratelimiter ratelimiter, joinpoint joinpoint) {
stringbuffer key = new stringbuffer(ratelimiter.key());
// 不同限流类型使用不同的前缀
switch (ratelimiter.limittype()) {
// xxx 可以新增通过参数指定参数进行限流
case ip:
key.append(iputil.getipaddr(((servletrequestattributes) objects.requirenonnull(requestcontextholder.getrequestattributes())).getrequest())).append(":");
break;
case user_id:
sysuserdetails user = securityutil.getuser();
if (!objectutils.isempty(user)) key.append(user.getuserid()).append(":");
break;
case global:
break;
}
methodsignature signature = (methodsignature) joinpoint.getsignature();
method method = signature.getmethod();
class<?> targetclass = method.getdeclaringclass();
key.append(targetclass.getsimplename()).append("-").append(method.getname());
return key.tostring();
}
④、编写lua脚本(两种将事件添加到redis的方法)
ⅰ:uuid(可用其他有相同的特性的值)为 zset 中的 value 值
- 参数介绍:
keys[1] = prefix : ? : classname : methodname
keys[2] = 唯一id
keys[3] = 当前时间
argv = [次数,单位时间,次数,单位时间, 次数, 单位时间 …] - 由 java传入分布式不重复的 value 值
-- 1. 获取参数
local key = keys[1]
local uuid = keys[2]
local currenttime = tonumber(keys[3])
-- 2. 以数组最大值为 ttl 最大值
local expiretime = -1;
-- 3. 遍历数组查看是否超过限流规则
for i = 1, #argv, 2 do
local raterulecount = tonumber(argv[i])
local rateruletime = tonumber(argv[i + 1])
-- 3.1 判断在单位时间内访问次数
local count = redis.call('zcount', key, currenttime - rateruletime, currenttime)
-- 3.2 判断是否超过规定次数
if tonumber(count) >= raterulecount then
return true
end
-- 3.3 判断元素最大值,设置为最终过期时间
if rateruletime > expiretime then
expiretime = rateruletime
end
end
-- 4. redis 中添加当前时间
redis.call('zadd', key, currenttime, uuid)
-- 5. 更新缓存过期时间
redis.call('pexpire', key, expiretime)
-- 6. 删除最大时间限度之前的数据,防止数据过多
redis.call('zremrangebyscore', key, 0, currenttime - expiretime)
return false
ⅱ、根据时间戳作为 zset 中的 value 值
- 参数介绍
keys[1] = prefix : ? : classname : methodname
keys[2] = 当前时间
argv = [次数,单位时间,次数,单位时间, 次数, 单位时间 …] - 根据时间进行生成 value 值,考虑同一毫秒添加相同时间值问题
以下为第二种实现方式,在并发高的情况下效率低,value 是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call(‘zadd’, key, currenttime, currenttime),但是在不冲突 value 的情况下,会比生成 uuid 好。
-- 1. 获取参数
local key = keys[1]
local currenttime = keys[2]
-- 2. 以数组最大值为 ttl 最大值
local expiretime = -1;
-- 3. 遍历数组查看是否越界
for i = 1, #argv, 2 do
local raterulecount = tonumber(argv[i])
local rateruletime = tonumber(argv[i + 1])
-- 3.1 判断在单位时间内访问次数
local count = redis.call('zcount', key, currenttime - rateruletime, currenttime)
-- 3.2 判断是否超过规定次数
if tonumber(count) >= raterulecount then
return true
end
-- 3.3 判断元素最大值,设置为最终过期时间
if rateruletime > expiretime then
expiretime = rateruletime
end
end
-- 4. 更新缓存过期时间
redis.call('pexpire', key, expiretime)
-- 5. 删除最大时间限度之前的数据,防止数据过多
redis.call('zremrangebyscore', key, 0, currenttime - expiretime)
-- 6. redis 中添加当前时间 ( 解决多个线程在同一毫秒添加相同 value 导致 redis 漏记的问题 )
-- 6.1 maxretries 最大重试次数 retries 重试次数
local maxretries = 5
local retries = 0
while true do
local result = redis.call('zadd', key, currenttime, currenttime)
if result == 1 then
-- 6.2 添加成功则跳出循环
break
else
-- 6.3 未添加成功则 value + 1 再次进行尝试
retries = retries + 1
if retries >= maxretries then
-- 6.4 超过最大尝试次数 采用添加随机数策略
local random_value = math.random(1, 1000)
currenttime = currenttime + random_value
else
currenttime = currenttime + 1
end
end
end
return false
⑤、编写aop拦截
@autowired
private redistemplate<string, object> redistemplate;
@autowired
private redisscript<boolean> limitscript;
/**
* 限流
* xxx 对限流要求比较高,可以使用在 redis中对规则进行存储校验 或者使用中间件
*
* @param joinpoint joinpoint
* @param ratelimiter 限流注解
*/
@before(value = "@annotation(ratelimiter)")
public void bobefore(joinpoint joinpoint, ratelimiter ratelimiter) {
// 1. 生成 key
string key = getcombinekey(ratelimiter, joinpoint);
try {
// 2. 执行脚本返回是否限流
boolean flag = redistemplate.execute(limitscript,
listutil.of(key, string.valueof(system.currenttimemillis())),
(object[]) getrules(ratelimiter));
// 3. 判断是否限流
if (boolean.true.equals(flag)) {
log.error("ip: '{}' 拦截到一个请求 rediskey: '{}'",
iputil.getipaddr(((servletrequestattributes) objects.requirenonnull(requestcontextholder.getrequestattributes())).getrequest()),
key);
throw new serviceexception(ratelimiter.message());
}
} catch (serviceexception e) {
throw e;
} catch (exception e) {
e.printstacktrace();
}
}
/**
* 获取规则
*
* @param ratelimiter 获取其中规则信息
* @return
*/
private long[] getrules(ratelimiter ratelimiter) {
int capacity = ratelimiter.rules().length << 1;
// 1. 构建 args
long[] args = new long[ratelimiter.preventduplicate() ? capacity + 2 : capacity];
// 3. 记录数组元素
int index = 0;
// 2. 判断是否需要添加防重复提交到redis进行校验
if (ratelimiter.preventduplicate()) {
raterule preventraterule = ratelimiter.preventduplicaterule();
args[index++] = preventraterule.count();
args[index++] = preventraterule.timeunit().tomillis(preventraterule.time());
}
raterule[] rules = ratelimiter.rules();
for (raterule rule : rules) {
args[index++] = rule.count();
args[index++] = rule.timeunit().tomillis(rule.time());
}
return args;
}到此这篇关于redis 多规则限流和防重复提交方案实现小结的文章就介绍到这了,更多相关redis 多规则限流和防重复提交内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论