SpringBoot 接口限流Lua脚本接合Redis 服务熔断 自定义注解 接口保护

介绍

Spring Boot 接口限流是防止接口被频繁请求而导致服务器负载过重或服务崩溃的一种策略。通过限流,我们可以控制单位时间内允许的请求次数,确保系统的稳定性。限流可以帮助防止恶意请求、保护系统资源,并优化 API 的可用性,避免因过多请求导致服务不可用。

Resis序列化

自定义注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME) //运行时使用
@Target({ElementType.METHOD}) // 应用到方法和类上
public @interface ApiLimitation {
    int seconds() default 5; //多少秒访问
    int maxCount() default 5; //最大次数
    //默认5秒可以访问5次
}

依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

配置文件

yml 复制代码
spring:
  redis:
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器端口号
    port: 6379
    # 使用的数据库索引,默认是0
    database: 0
    # 连接超时时间
    timeout: 1800000
    # 设置密码
    # password: "123456"
    lettuce:
      pool:
        # 最大阻塞等待时间,负数表示没有限制
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 5
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中最大连接数,负数表示没有限制
        max-active: 20

拦截器

java 复制代码
@Component
public class RequestInterceptor implements HandlerInterceptor {

    // RedisTemplate 用于与 Redis 交互
    private final RedisTemplate<Object, Object> redisTemplate;

    // 构造函数,注入 RedisTemplate
    public RequestInterceptor(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 检查处理的 handler 是否是 HandlerMethod(即具体的控制器方法)
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取方法上的 ApiLimitation 注解
            ApiLimitation methodAnnotation = handlerMethod.getMethodAnnotation(ApiLimitation.class);

            // 如果没有 ApiLimitation 注解,则跳过限流逻辑,允许访问
            if (methodAnnotation == null) {
                return true;
            }

            // 获取注解中的配置,设置时间窗口和最大访问次数
            int time = methodAnnotation.seconds(); // 限制的时间窗口(秒)
            int count = methodAnnotation.maxCount(); // 最大请求次数

            // 获取客户端的 IP 地址
            String ip = request.getRemoteAddr();

            // 组合 key,格式为 "ip:请求路径"
            String key = ip + ":" + request.getServletPath();
            List<Object> keys = Collections.singletonList(key);

            // 创建 Redis 脚本对象,用于执行 Lua 脚本
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(limitScriptText());  // 设置 Lua 脚本内容
            redisScript.setResultType(Long.class); // 设置返回值类型为 Long

            // 执行 Lua 脚本进行访问频率控制
            Long number = redisTemplate.execute(redisScript, keys, count, time);

            // 如果返回值为空或者访问次数超过最大限制,表示请求过于频繁,拒绝访问
            if (number == null || number.intValue() > count) {
                response.getWriter().write("访问频繁");  // 返回 "访问频繁" 信息给客户端
                return false;  // 拒绝访问
            }

            // 允许访问
            return true;
        }

        // 如果不是处理具体方法,默认允许访问
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    // 判断对象是否为 null 的工具方法
    public static boolean isNull(Object object) {
        return object == null;
    }

    // 返回用于限制访问频率的 Lua 脚本内容
    private String limitScriptText() {
        return "local key = KEYS[1]\n" +
                "local count = tonumber(ARGV[1])\n" +
                "local time = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key);\n" +
                "if current and tonumber(current) > count then\n" +  // 如果当前访问次数已经超过最大次数,则返回当前次数
                "    return tonumber(current);\n" +
                "end\n" +
                "current = redis.call('incr', key)\n" +  // 否则,增加访问次数
                "if tonumber(current) == 1 then\n" +  // 如果是第一次访问,设置 key 的过期时间
                "    redis.call('expire', key, time)\n" +  // 设置过期时间,避免 Redis 中的 key 永久存在
                "end\n" +
                "return tonumber(current);";  // 返回当前的访问次数
    }
}

注册拦截器

java 复制代码
@Configuration //表示该类为配置类
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final RequestInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
        //拦截所有的请求

//        registry.addInterceptor(interceptor)
//                .addPathPatterns("/user")//需要拦截的请求
//                .excludePathPatterns("/login");//不需要拦截的请求

    }
}

控制器

java 复制代码
@RestController  
public class UserController {

    @GetMapping("/info") 
    @ApiLimitation(seconds = 5,maxCount = 2) //五秒钟只可以访问2次
    public String getInfo(){

        return "成功";
    }
}
相关推荐
Asthenia041223 分钟前
为什么说MVCC无法彻底解决幻读的问题?
后端
Asthenia041224 分钟前
面试官问我:三级缓存可以解决循环依赖的问题,那两级缓存可以解决Spring的循环依赖问题么?是不是无法解决代理对象的问题?
后端
Asthenia041225 分钟前
面试复盘:使用 perf top 和火焰图分析程序 CPU 占用率过高
后端
Asthenia041226 分钟前
面试复盘:varchar vs char 以及 InnoDB 表大小的性能分析
后端
Asthenia041227 分钟前
面试问题解析:InnoDB中NULL值是如何记录和存储的?
后端
王磊鑫28 分钟前
重返JAVA之路-初识JAVA
java·开发语言
半兽先生1 小时前
WebRtc 视频流卡顿黑屏解决方案
java·前端·webrtc
Asthenia04121 小时前
面试官问我:TCP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia04121 小时前
HTTP 相比 TCP 的好处是什么?
后端
Asthenia04121 小时前
MySQL count(*) 哪个存储引擎更快?为什么 MyISAM 更快?
后端