@Auto-Annotation自定义注解——接口限流篇

  • Post author:
  • Post category:其他




@Auto-Annotation自定义注解——接口限流篇

自定义通用注解连更系列—连载中…


首页介绍:


点这里



前言

​ 在访问高峰期为保证应用服务稳定运行,需要对高并发场景下接口进行接口限流处理,通对接口流量的访问限制能够在一定程度上防止接口被恶意调用的情况出现。



所需依赖

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-redis</artifactId>
   <version>1.4.6.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>2.0.25</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
 <dependency>
   <groupId>cn.hutool</groupId>
   <artifactId>hutool-all</artifactId>
   <version>5.8.10</version>
</dependency>



接口限流注解@RateLimit

​ 自定义接口限流注解,主要定义四个参数,限流标识符,在一定时间内进行限流,访问达到多少次进行限流,限流类型是什么,全局限流还是根据IP限流。

/** 限流注解
 * @Author: 清峰
 * @Description: May there be no bug in the world!
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限流key
     */
    String key() default "rate_limit_key:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    RateLimitTypeEnum limitType() default RateLimitTypeEnum.GLOBAL;
}



定义LUA脚本

​ 通过redis加载lua脚本进行限流处理。

脚本逻辑:

1、首先获取到传进来的 key 以及 限流的 count 和时间 time。

2、通过 get 获取到这个 key 对应的值,这个值就是当前时间段内这个接口访问了多少次。

3、如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那就说明已经超过流量限制了,那么直接返回查询的结果即可。

4、如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。

5、最后把自增 1 后的值返回。

local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count then
    return tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
    redis.call('expire', key, time)
end
return tonumber(current)



加载redis配置

​ 自定义redis序列化模板,初始化lua脚本,脚本文件可自定义放置路径

/** 自定义RedisTemplate
 * @Author: 清峰
 * @Description: May there be no bug in the world!
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${auto.lua-path:lua/rateLimit.lua}")
    private String luaPath;
    /**
     * 自定义RedisTemplate
     * 直接使用默认的JdkSerializationRedisSerializer这个工具进行序列化时存放到redis中的key和value是会多一些前缀的
     * @param connectionFactory 连接工厂
     * @return 结果集
     */
    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

    /**
     * 限流脚本注入容器
     * @return 结果集
     */
    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    /**
     * 加载lua限流脚本
     */
    private ScriptSource limitScriptText() {
        return new ResourceScriptSource(new ClassPathResource(luaPath));
    }
}



定义限流类型

​ 支持全局限流,对接口进行限流操作。支持IP限流,精确到访问者IP,进行限流处理。

/**
 * @Author: 清峰
 * @Description: May there be no bug in the world!
 */
@Getter
@AllArgsConstructor
public enum RateLimitTypeEnum {
    /**
     * 全局限流
     */
    GLOBAL("GLOBAL","全局限流"),
    /**
     * IP限流
     */
    IP("IP","IP限流"),
    ;

    private final String value;
    private final String desc;

}



限流切面

​ 通过AOP对限流接口进行拦截,在接口方法执行前,查看redis中是否超过限流次数。操作访问次数则抛出异常信息,未超过访问次数则次数增加1位。

/**
 * 限流切面
 *
 * @Author: 清峰
 * @Description: May there be no bug in the world!
 */
@Slf4j
@Aspect
@Configuration
@ConditionalOnProperty(prefix = ConditionalConstants.AUTO_ENABLE,name = ConditionalConstants.RATE_LIMIT,havingValue = ConditionalConstants.ENABLE_TRUE)
public class RateLimiterAspect {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;
    @Resource
    private RedisScript<Long> limitScript;

    @Before("@annotation(rateLimit)")
    public void doBefore(JoinPoint point, RateLimit rateLimit) throws ServerException {
        //获取限流属性值
        int count = rateLimit.count();
        int time = rateLimit.time();
        try {
            //获取存入redis中的key
            String rateLimitKey = getRateLimitKey(rateLimit, point);
            //执行lua脚本获得返回值,访问次数
            Long number = redisTemplate.execute(limitScript, Collections.singletonList(rateLimitKey), count, time);
            if (ObjectUtil.isNull(number) || number.intValue() > count) {
                throw new ServerException("访问过于频繁,请稍后再试");
            }
        } catch (ServerException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍后再试");
        }
    }

    /**
     * 获取存入redis中的key
     * key -> 注解中配置的key前缀-ip地址-类名-方法名
     *
     * @param rateLimit 限流对象
     * @param point     切面对象
     * @return 结果集
     */
    private String getRateLimitKey(RateLimit rateLimit, JoinPoint point) {
        StringBuilder sb = new StringBuilder(rateLimit.key());
        if (RateLimitTypeEnum.IP.equals(rateLimit.limitType())) {
            sb.append(NetUtil.getLocalhostStr());
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> declaringClass = method.getDeclaringClass();
        sb.append(declaringClass.getName()).append(method.getName());
        return sb.toString();
    }

}



标记防重复提交接口

  @RateLimit(time = 30,count = 10,limitType = RateLimitTypeEnum.IP)
  @PostMapping("saveUser")
    private void saveUser(User user){
    System.out.println("保存用户信息逻辑...");
  }



总结

​ 至此接口限流完成,接口限流跟防重复提交篇实现方式类型,都是通过redis作为中间件存储数据充当标识符。略有不同的是一个通过拦截器实现,一个通过AOP实现。



版权声明:本文为weixin_45019350原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。