随着前后端分离架构的流⾏,前端页面与后端接口的调⽤关系越来越复杂,后端服务的稳定性越来越重要。在遇到突发的请求量激增,恶意的⽤户访问,亦或请求频率过⾼给下游服务带来较⼤压⼒时,我们常常需要通过缓存、限流、负载均衡等多种⽅式保证后端服务的稳定性。其中,限流是不可或缺的⼀环。

1. redis maven依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

2. redis yaml配置

      redis:
        host: 192.168.1.1
        port: 6379
        password: 
        jedis:
          pool:
            min-idle: 0
            max-idle: 8
            max-active: 80
            max-wait: 30000
            timeout: 3000

3. RedisConfig配置

    package com.dzy.tonguecollectionback.config;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * @Author 。。。源
     * @Email apple_dzy@163.com
     * @Blog https://www.findmyfun.cn
     * @Date 2022/6/6 17:22
     * @Version 1.0
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        @SuppressWarnings("all")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
    
            template.setConnectionFactory(factory);
    
            Jackson2JsonRedisSerializer 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);
    
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    
            // key采用String的序列化方式
    
            template.setKeySerializer(stringRedisSerializer);
    
            // hash的key也采用String的序列化方式
    
            template.setHashKeySerializer(stringRedisSerializer);
    
            // value序列化方式采用jackson
    
            template.setValueSerializer(jackson2JsonRedisSerializer);
    
            // hash的value序列化方式采用jackson
    
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
    
    
            template.afterPropertiesSet();
    
            return template;
    
        }
    }

4. WebConfig配置

    package com.dzy.tonguecollectionback.config;
    
    import com.dzy.tonguecollectionback.component.RequestLimitInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @Author 。。。源
     * @Email apple_dzy@163.com
     * @Blog https://www.findmyfun.cn
     * @Date 2022/11/14 15:24
     * @Version 1.0
     */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        private final RequestLimitInterceptor requestLimitInterceptor;
    
        public WebConfig(RequestLimitInterceptor requestLimitInterceptor) {
            this.requestLimitInterceptor = requestLimitInterceptor;
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(requestLimitInterceptor)
                    //拦截所有请求路径
                    .addPathPatterns("/**")
                    //再设置 放开哪些路径
    //                .excludePathPatterns("/static/**","/auth/login")
            ;
        }
    
    }

5. RequestLimit自定义注解

    package com.dzy.tonguecollectionback.annotation;
    
    import java.lang.annotation.*;
    
    /**
     * @Author 。。。源
     * @Email apple_dzy@163.com
     * @Blog https://www.findmyfun.cn
     * @Date 2022/11/14 15:25
     * @Version 1.0
     */
    @Inherited
    @Documented
    @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequestLimit {
    
        /**
         * 时间内  秒为单位
         */
        int second() default 10;
    
        /**
         *  允许访问次数
         */
        int maxCount() default 5;
    
    
        //默认效果 : 10秒内 对于使用该注解的接口,只能总请求访问数 不能大于 5次
    
    }

6. RequestLimitInterceptor拦截过滤器

    package com.dzy.tonguecollectionback.component;
    
    import com.alibaba.fastjson.JSONObject;
    import com.dzy.tonguecollectionback.annotation.RequestLimit;
    import com.dzy.tonguecollectionback.utils.IPUtils;
    import com.dzy.tonguecollectionback.utils.R;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.gson.JsonObject;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author 。。。源
     * @Email apple_dzy@163.com
     * @Blog https://www.findmyfun.cn
     * @Date 2022/11/14 15:26
     * @Version 1.0
     */
    @Component
    public class RequestLimitInterceptor implements HandlerInterceptor {
    
        private static Logger logger = LoggerFactory.getLogger(RequestLimitInterceptor.class);
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            try {
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    // 获取RequestLimit注解
                    RequestLimit requestLimit = handlerMethod.getMethodAnnotation(RequestLimit.class);
                    if (null == requestLimit) {
                        return true;
                    }
                    //限制的时间范围
                    int seconds = requestLimit.second();
                    //时间内的 最大次数
                    int maxCount = requestLimit.maxCount();
                    String ipAddr = IPUtils.getIpAddr(request);
                    // 存储key
                    String key = ipAddr + ":" + request.getContextPath() + ":" + request.getServletPath();
                    // 已经访问的次数
                    Integer count = (Integer) redisTemplate.opsForValue().get(key);
                    if (count == null) {
                        count = 0;
                    }
                    if (count == 0 || count == -1) {
                        count = 1;
                        logger.info("来源ip:{} 请求接口:{} 并发数:{}", ipAddr, request.getServletPath(), count);
                        redisTemplate.opsForValue().set(key, count, seconds, TimeUnit.SECONDS);
                        return true;
                    }
                    if (count < maxCount) {
                        logger.info("来源ip:{} 请求接口:{} 并发数:{}", ipAddr, request.getServletPath(), count + 1);
                        redisTemplate.opsForValue().increment(key);
                        return true;
                    }
                    logger.info("来源ip:{} 请求接口:{} 请求过于频繁", ipAddr, request.getServletPath());
                    returnData(response);
                    return false;
                }
                return true;
            } catch (Exception e) {
                logger.warn("异常请求 请求接口:{} 异常信息:{}", request.getServletPath(), e);
            }
            return true;
        }
    
        public void returnData(HttpServletResponse response) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            //这里传提示语可以改成自己项目的返回数据封装的类
            response.getWriter().println(new JSONObject(R.error().put("msg", "请求过于频繁请稍后再试")).toJSONString());
        }
    
    }