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 "成功";
    }
}
相关推荐
葫芦和十三几秒前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp27 分钟前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑1 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯2 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan4 小时前
多Agent之间的区别
后端
青石路6 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充6 小时前
1.面向对象设计思想
后端
IT_陈寒6 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro7 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗7 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端