SpringBoot如何使用自定义注解实现接口限流

目录
  • 使用自定义注解实现接口限流
    • 1、自定义限流注解
    • 2、限流类型枚举类
    • 3、限流 Lua 脚本
    • 4、限流切面处理类
    • 5、使用与测试
  • SpringBoot工程中限流方式
    • 1、google的guava,令牌桶算法实现限流
    • 2、interceptor+redis根据注解限流

使用自定义注解实现接口限流

在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流。

限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待。

1、自定义限流注解

import com.asurplus.common.enums.LimitType;
import java.lang.annotation.*;
/**
 * 限流注解
 *
 * @author Lizhou
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {
    /**
     * 限流key前缀
     */
    String prefix() default "limit:";
    /**
     * 限流时间,单位秒
     */
    int time() default 60;
    /**
     * 限流次数
     */
    int count() default 10;
    /**
     * 限流类型
     */
    LimitType type() default LimitType.DEFAULT;
}

2、限流类型枚举类

/**
 * 限流类型
 *
 * @author Lizhou
 */
public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}

我们定义了两种限流类型,分别为全局限流和 IP 限流,全局限流对访问接口的所有用户进行限流保护,IP 限流对每个 IP 请求用户进行单独限流保护。

3、限流 Lua 脚本

1、由于我们使用 Redis 进行限流,我们需要引入 Redis 的 maven 依赖,同时需要引入 aop 的依赖

<!-- aop依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在配置文件中配置 Redis 的连接信息,具体参考:SpringBoot中整合Redis

2、限流脚本

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
 * 接口限流
 */
@Slf4j
@Component
public class RedisLimitUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 限流
     *
     * @param key   键
     * @param count 限流次数
     * @param times 限流时间
     * @return
     */
    public boolean limit(String key, int count, int times) {
        try {
            String script = "local lockKey = KEYS[1]\n" +
                    "local lockCount = KEYS[2]\n" +
                    "local lockExpire = KEYS[3]\n" +
                    "local currentCount = tonumber(redis.call('get', lockKey) or \"0\")\n" +
                    "if currentCount < tonumber(lockCount)\n" +
                    "then\n" +
                    "    redis.call(\"INCRBY\", lockKey, \"1\")\n" +
                    "    redis.call(\"expire\", lockKey, lockExpire)\n" +
                    "    return true\n" +
                    "else\n" +
                    "    return false\n" +
                    "end";
            RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
            List<String> keys = Arrays.asList(key, String.valueOf(count), String.valueOf(times));
            return redisTemplate.execute(redisScript, keys);
        } catch (Exception e) {
            log.error("限流脚本执行失败:{}", e.getMessage());
        }
        return false;
    }
}

通过 Lua 脚本,根据 Redis 中缓存的键值判断限流时间(也是 key 的过期时间)内,访问次数是否超出了限流次数,没超出则访问次数 +1,返回 true,超出了则返回 false。

4、限流切面处理类

import com.asurplus.common.annotation.Limit;
import com.asurplus.common.enums.LimitType;
import com.asurplus.common.exception.CustomException;
import com.asurplus.common.ip.IpUtil;
import com.asurplus.common.redis.RedisLimitUtil;
import com.asurplus.common.utils.HttpRequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
 * 限流处理
 *
 * @author Lizhou
 */
@Slf4j
@Aspect
@Component
public class LimitAspect {
    @Autowired
    private RedisLimitUtil redisLimitUtil;
    /**
     * 前置通知,判断是否超出限流次数
     *
     * @param point
     */
    @Before("@annotation(limit)")
    public void doBefore(JoinPoint point, Limit limit) {
        try {
            // 拼接key
            String key = getCombineKey(limit, point);
            // 判断是否超出限流次数
            if (!redisLimitUtil.limit(key, limit.count(), limit.time())) {
                throw new CustomException("访问过于频繁,请稍候再试");
            }
        } catch (CustomException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("接口限流异常,请稍候再试");
        }
    }
    /**
     * 根据限流类型拼接key
     */
    public String getCombineKey(Limit limit, JoinPoint point) {
        StringBuilder sb = new StringBuilder(limit.prefix());
        // 按照IP限流
        if (limit.type() == LimitType.IP) {
            sb.append(IpUtil.getIpAddr(HttpRequestUtil.getRequest())).append("-");
        }
        // 拼接类名和方法名
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        sb.append(targetClass.getName()).append("-").append(method.getName());
        return sb.toString();
    }
}

1、使用我们刚刚的 Lua 脚本判断是否超出了限流次数,超出了限流次数后返回一个自定义异常,然后在全局异常中去捕捉异常,返回 JSON 数据。

2、根据注解参数,判断限流类型,拼接缓存 key 值

5、使用与测试

1、测试方法

@Limit(type = LimitType.DEFAULT, time = 10, count = 2)
@GetMapping("test")
public String test() {
    return "请求成功:" + System.currentTimeMillis();
}

使用自定义注解 @Limit,限制为 10 秒内,允许访问 2 次

2、测试结果

第一次

第二次

第三次

可以看出,前面两次都成功返回了请求结果,第三次超出了接口限流次数,返回了自定义异常信息。

SpringBoot工程中限流方式

限流,是防止用户恶意刷新接口。常见的限流方式有阿里开源的sentinel、redis等。

1、google的guava,令牌桶算法实现限流

Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

// RateLimiter提供了两个工厂方法,最终会调用下面两个函数,生成RateLimiter的两个子类。
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
     RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
     return rateLimiter;
}
static RateLimiter create(
     SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit,
 double coldFactor) {
     RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
    rateLimiter.setRate(permitsPerSecond);
     return rateLimiter;
}
  • 平滑突发限流:使用 RateLimiter的静态方法创建一个限流器,设置每秒放置的令牌数为10个。返回的RateLimiter对象可以保证1秒内不会给超过10个令牌,并且以固定速率进行放置,达到平滑输出的效果。
  • 平滑预热限流:RateLimiter的 SmoothWarmingUp是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。
@RestController
public class HomeController {
    // 这里的10表示每秒允许处理的量为10个
    private RateLimiter limiter = RateLimiter.create(10);
    private RateLimiter limiter2 = RateLimiter.create(2, 1000, TimeUnit.SECONDS);
    //permitsPerSecond: 表示 每秒新增 的令牌数;warmupPeriod: 表示在从 冷启动速率 过渡到 平均速率 的时间间隔
    @GetMapping("/test/{name}")
    public String test(@PathVariable("name") String name) {
        // 请求RateLimiter, 超过permits会被阻塞
        final double acquire = limiter.acquire();
        System.out.println("acquire=" + acquire);
        //判断double是否为空或者为0
        if (acquire == 0) {
            return name;
        } else {
            return "操作太频繁";
        }
    }
    @AccessLimit(limit = 2, sec = 10)
    @GetMapping("/test2/{name}")
    public String test2(@PathVariable("name") String name) {
        return name;
    }
}

2、interceptor+redis根据注解限流

public class AccessLimitInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Integer> redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if (!method.isAnnotationPresent(AccessLimit.class)) {
                return true;
            }
            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            int sec = accessLimit.sec();
            String key = IPUtil.getIpAddress(request) + request.getRequestURI();
            //资源唯一标识
            Integer maxLimit = redisTemplate.opsForValue().get(key);
            if (maxLimit == null) {
                //set时一定要加过期时间
                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);
            } else if (maxLimit < limit) {
                redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);
            } else {
                output(response, "请求太频繁!");
                return false;
            }
        }
        return true;
    }
    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Bean
    public AccessLimitInterceptor accessLimitInterceptor() {
        return new AccessLimitInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //addPathPatterns 添加拦截规则
        registry.addInterceptor(accessLimitInterceptor()).addPathPatterns("/**");
    }
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/");
    }
}
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

限流方式还有很多,后续继续尝试。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpingBoot中使用Redis对接口进行限流的实现

    目录 实现的思路 使用 Hash 存储接口的限流配置 使用普通kv,存储api的请求次数 使用SpringBoot实现 RedisKeys ObjectRedisTemplate RedisConfigration RequestLimitConfig RequestLimitInterceptor Controller WebMvcConfigration 最后一些问题 怎么灵活的配置 针对指定的用户限流 Restful 接口的问题 一个基于Redis实现的接口限流方案,先说要实现的功能 可以

  • Springboot使用redis实现接口Api限流的实例

    前言 该篇介绍的内容如题,就是利用redis实现接口的限流(  某时间范围内 最大的访问次数 ) . 正文 惯例,先看下我们的实战目录结构: 首先是pom.xml 核心依赖: <!--用于redis数据库连接--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId>

  • SpringBoot 进行限流的操作方法

    目录 为什么要进行限流? 什么是限流?有哪些限流算法? 1. 计数器限流 2. 漏桶算法 3. 令牌桶算法 基于Guava工具类实现限流 基于AOP实现接口限流 小结 大家好,我是飘渺.SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算法,如何优雅的进行限流(基于AOP). 首先就让我们来看看为什么需要对接口进行限流? 为什么要进行限流? 因为互联网系统通常都要面对

  • SpringBoot如何使用自定义注解实现接口限流

    目录 使用自定义注解实现接口限流 1.自定义限流注解 2.限流类型枚举类 3.限流 Lua 脚本 4.限流切面处理类 5.使用与测试 SpringBoot工程中限流方式 1.google的guava,令牌桶算法实现限流 2.interceptor+redis根据注解限流 使用自定义注解实现接口限流 在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流. 限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务.排队或等待. 1.

  • SpringBoot使用自定义注解+AOP+Redis实现接口限流的实例代码

    目录 为什么要限流 限流背景 实现限流 1.引入依赖 2.自定义限流注解 3.限流切面 4.写一个简单的接口进行测试 5.全局异常拦截 6.接口测试 为什么要限流 系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用.为了避免这种情况,我们就需要对接口请求进行限流. 所以,我们可以通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统或避免不必要的资源浪费,一旦达到限制速率则可以拒绝服务.排队或等待

  • SpringBoot基于redis自定义注解实现后端接口防重复提交校验

    目录 一.添加依赖 二.代码实现 三.测试 一.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.4.RELEASE</version> </dependency> <dependency> <

  • springboot中shiro使用自定义注解屏蔽接口鉴权实现

    目录 传统做法 使用自定义注解屏蔽接口鉴权 拓展内容:关于spring中的派生注解 传统做法 spring boot整合shiro后,如果某些接口需要屏蔽鉴权的话(比如登录)接口,我们一般会这么做: @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFil

  • 使用SpringBoot + Redis 实现接口限流的方式

    目录 配置 限流注解 定制 RedisTemplate Lua 脚本 注解解析 接口测试 全局异常处理 Redis 除了做缓存,还能干很多很多事情:分布式锁.限流.处理请求接口幂等性...太多太多了 配置 首先我们创建一个 Spring Boot 工程,引入 Web 和 Redis 依赖,同时考虑到接口限流一般是通过注解来标记,而注解是通过 AOP 来解析的,所以我们还需要加上 AOP 的依赖,最终的依赖如下: <dependency> <groupId>org.springfra

  • SpringBoot Redis用注释实现接口限流详解

    目录 1. 准备工作 2. 限流注解 3. 定制 RedisTemplate 4. 开发 Lua 脚本 5. 注解解析 6. 接口测试 7. 全局异常处理 1. 准备工作 首先我们创建一个 Spring Boot 工程,引入 Web 和 Redis 依赖,同时考虑到接口限流一般是通过注解来标记,而注解是通过 AOP 来解析的,所以我们还需要加上 AOP 的依赖,最终的依赖如下: <dependency> <groupId>org.springframework.boot</g

  • Springboot+redis+Interceptor+自定义annotation实现接口自动幂等

    前言: 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同.按照这个含义,最终的含义就是对数据库的影响只能是一次性的,不能重复处理.如何保证其幂等性,通常有以下手段: 1:数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据 2:token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断toke

  • SpringBoot如何通过自定义注解实现权限检查详解

    前言 最近开发了一个接口,完成后准备自测时,却被拦截器拦截了,提示:(AUTH-NO)未能获得有效的请求参数!怎么会这样呢? 于是我全局搜了这个提示语,结果发现它被出现在一个Aspect类当中了,并且把一个 @interface 作为了一个切点,原来这里利用了Spring AOP面向切面的方式进行权限控制. SpringBoot通过自定义注解实现日志打印可参考:SpringBoot通过自定义注解实现日志打印 正文 Spring AOP Spring AOP 即面向切面,是对OOP面向对象的一种延

  • SpringBoot服务上实现接口限流的方法

    Sentinel是阿里巴巴开源的限流器熔断器,并且带有可视化操作界面. 在日常开发中,限流功能时常被使用,用于对某些接口进行限流熔断,譬如限制单位时间内接口访问次数:或者按照某种规则进行限流,如限制ip的单位时间访问次数等. 之前我们已经讲过接口限流的工具类ratelimter可以实现令牌桶的限流,很明显sentinel的功能更为全面和完善.来看一下sentinel的简介: https://github.com/spring-cloud-incubator/spring-cloud-alibab

  • SpringBoot基于Sentinel在服务上实现接口限流

    Sentinel是阿里巴巴开源的限流器熔断器,并且带有可视化操作界面. 在日常开发中,限流功能时常被使用,用于对某些接口进行限流熔断,譬如限制单位时间内接口访问次数:或者按照某种规则进行限流,如限制ip的单位时间访问次数等. 之前我们已经讲过接口限流的工具类ratelimter可以实现令牌桶的限流,很明显sentinel的功能更为全面和完善.来看一下sentinel的简介: https://github.com/spring-cloud-incubator/spring-cloud-alibab

随机推荐