Redis的实现简易限流的两种方案(基于自定义注解+SpringBoot拦截器)
一、基于Redis的 String 结构
这里为什么会想到实现这个功能,首先是前段时间看到有人恶意访问博客的评论接口,大量刷取评论,一秒钟请求了上千次写数据库的操作,由于博客网站也是比较简陋,果然项目只有跑起来的时候才是最舒服的,后续基本也没有维护(博客也基本没有再写了),当时就只是把这几千条数据删除了。这几天看代码的时候,看到了Redis部分的代码,加上实习公司月度技术分享的时候 展示了一下自定义注解配合拦截器,让我想到也可以通过自定义注解 加 Redis 实现,顺便学习一波注解相关知识。
先说一下最基本思路:使用Redis String 结构,key 存储用户ip,value 存储访问次数 配合一个过期时间,然后取出访问次数,超出访问次数就禁止访问。
代码实现:
首先实现自定义注解
// 三个元注解
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 保留注解到运行时
@Documented // 生产文档注解 (可忽略)
public @interface AccessLimit {
// 定义的两个注解参数
/**
* 最大允许访问数量
*/
int maxCount();
/**
* 单位时间(秒)
* @return
*/
int seconds();
}
// 使用 直接作用在方法上 填入参数
@AccessLimit(maxCount = 2,seconds = 20)
然后实现 SpringBoot 自带的 HandlerInterceptor 接口 (这里也可以采用 AOP 的方式切入)
// 标记为Spirng 组件
@Component
public class WebSecurityInterceptor implements HandlerInterceptor {
// 重写 preHandle 在方法执行之前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 等会这里重写逻辑
return true;
}
}
然后还需要将该拦截器添加到配置中
// 采用配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private WebSecurityInterceptor webSecurityInterceptor;
// 添加拦截器 (如果多个拦截器 会按照添加顺序进行拦截)
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(webSecurityInterceptor);
}
}
最后就是 实现 Redis String 限流方案
// 在刚刚重写的方法中
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否属于方法handler
if (handler instanceof HandlerMethod) {
// 获取判断是否含有注解
AccessLimit accessLimit = ((HandlerMethod) handler).getMethodAnnotation(AccessLimit.class);
// 没有注解标记 直接返回允许通行
if (accessLimit == null) {
return true;
}
// 取出注解参数
int maxCount = accessLimit.maxCount();
int seconds = accessLimit.seconds();
// 获取当前访问用户的ip 实现对用户级别的限流
String ip = request.getRemoteAddr();
// 以访问路径和用户ip拼接key
String key = request.getServletPath() + ip;
// 从redis 中获取当前用户记录
Integer count = (Integer) redisTemplate.opsForValue().get(key);
// 如果第一次访问
if (count == null || count == -1) {
// 设置为一 并设置时间
redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
return true;
}
// 如果小于 则直接加一
if (count < maxCount) {
redisTemplate.opsForValue().increment(key, 1);
return true;
}
// 大于 限流 返回错误信息
render(response, new R().fail("操作过于频繁,请稍后再试"));
return false;
}
return true;
}
/**
* 给页面返回错误信息
*/
private void render(HttpServletResponse response, R result) {
response.setContentType("application/json; charset=utf-8");
OutputStream out = null;
try {
out = response.getOutputStream();
String str = JSON.toJSONString(result);
out.write(str.getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
二、基于Redis Zset 结构 以滑动窗口的方式 实现 单位时间内对 接口的限流
方案一中有缺陷,所以是针对 ip 进行限流,因为只能 当统计 1- 11秒的时候,没法统计 2-12 秒 就是没法统计 N 秒内 M 个请求(如果要做到 就需要多个key)
基本思路: 使用 Redis 的 Zset 因为 Zset 天然按照 score 进行排序,使用 methodName 作为 Key ,当前时间戳作为 score,在每次查询的时候 动态的维护时间窗口,将不属于 当面限制时间段内的数据给清除,统计属于当前时间段内的次数即可
具体看代码实现
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否属于方法handler
if (handler instanceof HandlerMethod) {
// 获取判断是否含有注解
AccessLimit accessLimit = ((HandlerMethod) handler).getMethodAnnotation(AccessLimit.class);
// 没有注解标记 直接返回允许通行
if (accessLimit == null) {
return true;
}
// 获取 限流的参数
int maxCount = accessLimit.maxCount();
int seconds = accessLimit.seconds();
这里区分和方案一不同
---------------
// 获取方法名 这里实现对方法级别的限制访问
String methodName = ((HandlerMethod) handler).getMethod().getName();
// 获取当前时间戳
long nowTime = new Date().getTime();
// 设置方法访问的 时间戳
redisTemplate.opsForZSet().add(methodName, nowTime + " ", nowTime);
// 删除窗口之外的数据
redisTemplate.opsForZSet().removeRangeByScore(methodName, 0, nowTime - seconds * 1000);
// 获取窗口内的访问次数
Long count = redisTemplate.opsForZSet().zCard(methodName);
-----------------
// 如果超出访问限制 限流
if (count > maxCount) {
render(response, new R().fail("操作过于频繁,请稍后再试"));
return false;
}
}
return true;
}