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 "成功";
    }
}
相关推荐
等等543几秒前
Java EE初阶——初识多线程
java·开发语言·jvm
斯普信专业组6 分钟前
Elasticsearch内存管理与JVM优化:原理剖析与最佳实践
大数据·jvm·elasticsearch
lyrhhhhhhhh27 分钟前
JDBC工具类的三个版本
数据库
星霜旅人31 分钟前
Java并发编程
java
正在走向自律43 分钟前
【金仓数据库征文】政府项目数据库迁移:从MySQL 5.7到KingbaseES的蜕变之路
数据库·mysql·kingbasees·金仓数据库 2025 征文·数据库平替用金仓
天上掉下来个程小白1 小时前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
大慕慕好懒1 小时前
redis未授权访问
redis·网络安全·渗透·ssrf
深色風信子1 小时前
Eclipse 插件开发 6 右键菜单
java·ide·eclipse·右键菜单
网安INF1 小时前
Apache Shiro 1.2.4 反序列化漏洞(CVE-2016-4437)
java·网络安全·apache
RedJACK~2 小时前
Go语言Stdio传输MCP Server示例【Cline、Roo Code】
开发语言·后端·golang