做棋牌网站多少钱,合肥房产网,做模拟人生类的游戏下载网站,做影视网站引流一、思路
使用接口限流的主要目的在于提高系统的稳定性#xff0c;防止接口被恶意打击#xff08;短时间内大量请求#xff09;。
比如要求某接口在1分钟内请求次数不超过1000次#xff0c;那么应该如何设计代码呢#xff1f;
下面讲两种思路#xff0c;如果想看代码可…一、思路
使用接口限流的主要目的在于提高系统的稳定性防止接口被恶意打击短时间内大量请求。
比如要求某接口在1分钟内请求次数不超过1000次那么应该如何设计代码呢
下面讲两种思路如果想看代码可直接翻到后面的代码部分。
1.1 固定时间段旧思路
1.1.1 思路描述
该方案的思路是使用Redis记录固定时间段内某用户IP访问某接口的次数其中 Redis的key用户IP 接口方法名 Redis的value当前接口访问次数。
当用户在近期内第一次访问该接口时向Redis中设置一个包含了用户IP和接口方法名的keyvalue的值初始化为1表示第一次访问当前接口。同时设置该key的过期时间比如为60秒。
之后只要这个key还未过期用户每次访问该接口都会导致value自增1次。
用户每次访问接口前先从Redis中拿到当前接口访问次数如果发现访问次数大于规定的次数如超过1000次则向用户返回接口访问失败的标识。 1.1.2 思路缺陷
该方案的缺点在于限流时间段是固定的。
比如要求某接口在1分钟内请求次数不超过1000次观察以下流程 可以发现00:59和01:01之间仅仅间隔了2秒但接口却被访问了10009991999次是限流次数1000次的2倍
所以在该方案中限流次数的设置可能不起作用仍然可能在短时间内造成大量访问。
1.2 滑动窗口新思路
1.2.1 思路描述
为了避免出现方案1中由于键过期导致的短期访问量增大的情况我们可以改变一下思路也就是把固定的时间段改成动态的
假设某个接口在10秒内只允许访问5次。用户每次访问接口时记录当前用户访问的时间点时间戳并计算前10秒内用户访问该接口的总次数。如果总次数大于限流次数则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过1000次。
如下图假设用户在0:19时间点访问接口经检查其前10秒内访问次数为5次则允许本次访问。 假设用户0:20时间点访问接口经检查其前10秒内访问次数为6次超出限流次数5次则不允许本次访问。 1.2.2 Redis部分的实现
1选用何种 Redis 数据结构
首先是需要确定使用哪个Redis数据结构。用户每次访问时需要用一个key记录用户访问的时间点而且还需要利用这些时间点进行范围检查。
2为何选择 zSet 数据结构
为了能够实现范围检查可以考虑使用Redis中的zSet有序集合。
添加一个zSet元素的命令如下
ZADD [key] [score] [member]它有一个关键的属性score通过它可以记录当前member的优先级。
于是我们可以把score设置成用户访问接口的时间戳以便于通过score进行范围检查。key则记录用户IP和接口方法名至于member设置成什么没有影响一个member记录了用户访问接口的时间点。因此member也可以设置成时间戳。
3zSet 如何进行范围检查检查前几秒的访问次数
思路是把特定时间间隔之前的member都删掉留下的member就是时间间隔之内的总访问次数。然后统计当前key中的member有多少个即可。
① 把特定时间间隔之前的member都删掉。
zSet有如下命令用于删除score范围在[min~max]之间的member
Zremrangebyscore [key] [min] [max]假设限流时间设置为5秒当前用户访问接口时获取当前系统时间戳为currentTimeMill那么删除的score范围可以设置为
min 0
max currentTimeMill - 5 * 1000相当于把5秒之前的所有member都删除了只留下前5秒内的key。
② 统计特定key中已存在的member有多少个。
zSet有如下命令用于统计某个key的member总数 ZCARD [key]统计的key的member总数就是当前接口已经访问的次数。如果该数目大于限流次数则说明当前的访问应被限流。
二、代码实现
主要是使用注解 AOP的形式实现。
2.1 固定时间段思路
使用了lua脚本。 参考https://blog.csdn.net/qq_43641418/article/details/127764462 2.1.1 限流注解
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface RateLimiter {/*** 限流时间单位秒*/int time() default 5;/*** 限流次数*/int count() default 10;
}2.1.2 定义lua脚本
在resources/lua下新建limit.lua
-- 获取redis键
local key KEYS[1]
-- 获取第一个参数次数
local count tonumber(ARGV[1])
-- 获取第二个参数时间
local time tonumber(ARGV[2])
-- 获取当前流量
local current redis.call(get, key);
-- 如果current值存在且值大于规定的次数则拒绝放行直接返回当前流量
if current and tonumber(current) count thenreturn tonumber(current)
end
-- 如果值小于规定次数或值不存在则允许放行当前流量数1 (值不存在情况下可以自增变为1)
current redis.call(incr, key);
-- 如果是第一次进来那么开始设置键的过期时间。
if tonumber(current) 1 then redis.call(expire, key, time);
end
-- 返回当前流量
return tonumber(current)2.1.3 注入Lua执行脚本
关键代码是limitScript()方法
Configuration
public class RedisConfig {Beanpublic RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplateObject, Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(connectionFactory);// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)Jackson2JsonRedisSerializerObject jackson2JsonRedisSerializer new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);return redisTemplate;}/*** 解析lua脚本的bean*/Bean(limitScript)public DefaultRedisScriptLong limitScript() {DefaultRedisScriptLong redisScript new DefaultRedisScript();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(lua/limit.lua)));redisScript.setResultType(Long.class);return redisScript;}
}2.1.3 定义Aop切面类
Slf4j
Aspect
Component
public class RateLimiterAspect {Autowiredprivate RedisTemplate redisTemplate;Autowiredprivate RedisScriptLong limitScript;Before(annotation(rateLimiter))public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {int time rateLimiter.time();int count rateLimiter.count();String combineKey getCombineKey(rateLimiter.type(), point);ListString keys Collections.singletonList(combineKey);try {Long number (Long) redisTemplate.execute(limitScript, keys, count, time);// 当前流量number已超过限制则抛出异常if (number null || number.intValue() count) {throw new RuntimeException(访问过于频繁请稍后再试);}log.info([limit] 限制请求数{},当前请求数{},缓存key{}, count, number.intValue(), combineKey);} catch (Exception ex) {ex.printStackTrace();throw new RuntimeException(服务器限流异常请稍候再试);}}/*** 把用户IP和接口方法名拼接成 redis 的 key* param point 切入点* return 组合key*/private String getCombineKey(JoinPoint point) {StringBuilder sb new StringBuilder(rate_limit:);ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request attributes.getRequest();sb.append( Utils.getIpAddress(request) );MethodSignature signature (MethodSignature) point.getSignature();Method method signature.getMethod();Class? targetClass method.getDeclaringClass();// keyPrefix - class - methodreturn sb.append(-).append( targetClass.getName() ).append(-).append(method.getName()).toString();}
}2.2 滑动窗口思路
2.2.1 限流注解
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface RateLimiter {/*** 限流时间单位秒*/int time() default 5;/*** 限流次数*/int count() default 10;
}2.2.2 定义Aop切面类
Slf4j
Aspect
Component
public class RateLimiterAspect {Autowiredprivate RedisTemplate redisTemplate;/*** 实现限流新思路* param point* param rateLimiter* throws Throwable*/SuppressWarnings(unchecked)Before(annotation(rateLimiter))public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {// 在 {time} 秒内仅允许访问 {count} 次。int time rateLimiter.time();int count rateLimiter.count();// 根据用户IP可选和接口方法构造keyString combineKey getCombineKey(rateLimiter.type(), point);// 限流逻辑实现ZSetOperations zSetOperations redisTemplate.opsForZSet();// 记录本次访问的时间结点long currentMs System.currentTimeMillis();zSetOperations.add(combineKey, currentMs, currentMs);// 这一步是为了防止member一直存在于内存中redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);// 移除{time}秒之前的访问记录滑动窗口思想zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000);// 获得当前窗口内的访问记录数Long currCount zSetOperations.zCard(combineKey);// 限流判断if (currCount count) {log.error([limit] 限制请求数{},当前请求数{},缓存key{}, count, currCount, combineKey);throw new RuntimeException(访问过于频繁请稍后再试!);}}/*** 把用户IP和接口方法名拼接成 redis 的 key* param point 切入点* return 组合key*/private String getCombineKey(JoinPoint point) {StringBuilder sb new StringBuilder(rate_limit:);ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request attributes.getRequest();sb.append( Utils.getIpAddress(request) );MethodSignature signature (MethodSignature) point.getSignature();Method method signature.getMethod();Class? targetClass method.getDeclaringClass();// keyPrefix - class - methodreturn sb.append(-).append( targetClass.getName() ).append(-).append(method.getName()).toString();}
}