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

@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实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用Redisson提供的分布式限流工具来实现自定义注解接口限流。具体实现步骤如下: 1. 定义一个注解,例如@RateLimiter,用于标记需要进行限流接口方法。 2. 在注解定义相关属性,例如限流的速率、限流的时间单位等。 3. 使用AOP技术,在接口方法执行前判断当前请求是否超过了限流速率,如果超过则拒绝请求。 4. 在AOP切面中使用Redisson提供的分布式锁来实现限流。 下面是一个简单的示例代码: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { int rate() default 10; // 速率 TimeUnit timeUnit() default TimeUnit.SECONDS; // 时间单位 } @Aspect @Component public class RateLimiterAspect { private final RedissonClient redissonClient; public RateLimiterAspect(RedissonClient redissonClient) { this.redissonClient = redissonClient; } @Around("@annotation(rateLimiter)") public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { String key = "rate_limiter:" + joinPoint.getSignature().toLongString(); RRateLimiter limiter = redissonClient.getRateLimiter(key); limiter.trySetRate(RateType.OVERALL, rateLimiter.rate(), rateLimiter.timeUnit()); if (limiter.tryAcquire()) { return joinPoint.proceed(); } else { throw new RuntimeException("接口访问过于频繁,请稍后再试!"); } } } ``` 在上面的代码中,我们定义了一个@RateLimiter注解,并在AOP切面中使用Redisson提供的RRateLimiter来实现限流。在接口方法执行前,我们会先获取到当前接口方法的签名,然后使用签名作为Redisson的key,创建一个RRateLimiter对象,并设置限流速率。最后,我们使用tryAcquire()方法来尝试获取锁,如果获取成功则执行接口方法,否则抛出异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值