Redis的实现简易限流的两种方案

279 阅读2分钟

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;
    }