SpringBoot实战:高效实现API限流策略

一、Guava库

在SpringBoot项目中实现限流,我们可以使用多种策略,如基于令牌桶(Token Bucket)的Guava RateLimiter,或者基于漏桶(Leaky Bucket)算法的自定义实现,甚至是基于Redis的分布式限流。以下是一个使用Guava RateLimiter在SpringBoot项目中实现限流的完整示例。

1. 引入依赖

首先,在pom.xml文件中添加Guava库的依赖:

XML 复制代码
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Guava for RateLimiter -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>31.0.1-jre</version> <!-- 请使用最新版本 -->
    </dependency>
    <!-- 其他依赖项 -->
</dependencies>

2. 创建RateLimiter配置类

创建一个配置类来初始化RateLimiter,并将其作为Bean注入到其他组件中:

java 复制代码
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RateLimiterConfig {

    @Bean
    public RateLimiter apiRateLimiter() {
        // 每秒允许10个请求
        return RateLimiter.create(10.0);
    }
}

3. 创建控制器类

在控制器类中注入RateLimiter,并在请求处理方法中使用它:

java 复制代码
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/api")
public class ApiController {

    @Autowired
    private RateLimiter apiRateLimiter;

    @GetMapping("/limited")
    public String limitedEndpoint() {
        // 尝试获取许可,如果失败则等待一段时间再重试(可选)
        boolean acquired = false;
        try {
            acquired = apiRateLimiter.tryAcquire(1, TimeUnit.SECONDS); // 等待最多1秒获取许可
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        }

        if (acquired) {
            return "Request processed successfully";
        } else {
            return "Too many requests - try again later";
        }
    }
}

在这个例子中,limitedEndpoint方法会尝试从RateLimiter中获取一个许可。如果获取成功,则返回成功响应;如果获取失败(即请求被限流),则返回一个错误响应。注意,这里使用了tryAcquire(long timeout, TimeUnit unit)方法来允许请求在指定的时间内等待获取许可,但这在实际应用中可能会导致请求的延迟增加。如果不希望等待,可以直接使用tryAcquire()方法。

二、使用Redis实现分布式限流

1. 引入依赖

首先,在你的pom.xml文件中添加Spring Boot Starter Data Redis的依赖:

XML 复制代码
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Starter Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 其他依赖项 -->
</dependencies>

2. 配置Redis

application.propertiesapplication.yml文件中配置Redis连接信息:

XML 复制代码
# application.properties
spring.redis.host=localhost
spring.redis.port=6379
# 如果Redis设置了密码,请取消以下行的注释并设置密码
# spring.redis.password=yourpassword

或者

XML 复制代码
# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    # 如果Redis设置了密码,请设置密码
    # password: yourpassword

3. 创建Redis限流服务

创建一个服务类,用于实现限流逻辑:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RateLimiterService {

    private static final String KEY_PREFIX = "rate_limiter:";
    private static final long LIMIT_PERIOD = 60; // 限流周期,单位:秒
    private static final int LIMIT_COUNT = 10; // 限流阈值

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String userId) {
        String key = KEY_PREFIX + userId;
        ValueOperations<String, String> ops = redisTemplate.opsForValue();

        // 获取当前请求时间戳
        long currentTime = System.currentTimeMillis();
        // 获取上一次请求时间戳,如果不存在则为-1
        String lastRequestTimeStr = ops.get(key);
        long lastRequestTime = lastRequestTimeStr != null ? Long.parseLong(lastRequestTimeStr) : -1;

        if (lastRequestTime == -1 || (currentTime - lastRequestTime) > TimeUnit.SECONDS.toMillis(LIMIT_PERIOD)) {
            // 如果上一次请求时间不存在,或者当前时间与上一次请求时间之差超过了限流周期,则重置计数器
            ops.set(key, String.valueOf(currentTime));
            ops.expire(key, LIMIT_PERIOD, TimeUnit.SECONDS); // 设置键的过期时间
            return true;
        } else {
            // 获取当前周期内的请求次数
            String requestCountStr = ops.get(key + ":count");
            int requestCount = requestCountStr != null ? Integer.parseInt(requestCountStr) : 0;

            if (requestCount < LIMIT_COUNT) {
                // 如果当前周期内的请求次数未达到阈值,则允许请求并增加计数器
                ops.increment(key + ":count", 1);
                return true;
            } else {
                // 如果当前周期内的请求次数已达到阈值,则拒绝请求
                return false;
            }
        }
    }
}

注意:上面的代码实现了一个简单的滑动窗口限流算法,但它有一个问题,即在Redis中存储了两个键(一个用于存储最后请求时间,另一个用于存储请求计数)。这可能会导致在极端情况下(如Redis崩溃并重启后)的数据不一致性。为了简化示例,这里采用了这种方法。在生产环境中,你可能需要更复杂的实现,比如使用Lua脚本来原子地执行这些操作,或者使用更高级的限流算法(如令牌桶或漏桶算法)。

4. 创建控制器

创建一个控制器类,用于处理API请求并调用限流服务:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ApiController {

    @Autowired
    private RateLimiterService rateLimiterService;

    @GetMapping("/limited")
    public String limitedEndpoint(@RequestParam String userId) {
        if (rateLimiterService.isAllowed(userId)) {
            return "Request processed successfully";
        } else {
            return "Too many requests - try again later";
        }
    }
}

三、Resilience4j限流

Resilience4j 是一个轻量级的、易于使用的容错库,它提供了多种容错机制,包括限流(Rate Limiter)。Resilience4j 的限流器可以帮助你控制对某个服务的并发访问数量,从而防止系统过载。

以下是一个使用 Resilience4j 限流器结合 Spring Boot 的完整代码示例:

1. 引入依赖

首先,在你的 pom.xml 文件中添加 Resilience4j 和 Spring Boot Starter 的依赖:

XML 复制代码
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Resilience4j Spring Boot2 Starter -->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot2</artifactId>
        <version>你的版本号</version>
    </dependency>

    <!-- 其他依赖项 -->
</dependencies>

2. 配置 Resilience4j 限流器

application.ymlapplication.properties 文件中配置 Resilience4j 限流器:

XML 复制代码
resilience4j.ratelimiter:
  instances:
    backendA:
      limitForPeriod: 10 # 在指定时间窗口内允许的请求数量
      limitRefreshPeriod: 10s # 时间窗口大小
      timeoutDuration: 0 # 可选,请求超时时间

3. 创建限流配置类

创建一个配置类来定义和配置 Resilience4j 的限流器实例:

java 复制代码
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.annotation.RateLimiterConfigProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RateLimiterConfig {

    @Bean
    public RateLimiterConfig rateLimiterConfig() {
        return RateLimiterConfig.custom()
                // 这里可以配置自定义的限流参数,如果已经在 application.yml 中配置了,则不需要在这里重复配置
                .build();
    }

    @Bean
    public RateLimiterRegistry rateLimiterRegistry(RateLimiterConfig rateLimiterConfig) {
        return RateLimiterRegistry.of(rateLimiterConfig);
    }

    // 如果你想要通过代码方式配置限流器实例,而不是依赖 application.yml,可以这样做:
    /*
    @Bean
    @RateLimiterConfigProperties(name = "backendA")
    public RateLimiter backendARateLimiter(RateLimiterRegistry rateLimiterRegistry) {
        return rateLimiterRegistry.rateLimiter("backendA");
    }
    */
}

注意:上面的 RateLimiterConfigRateLimiterRegistry Bean 是为了展示如何通过代码配置 Resilience4j,但在这个例子中,我们实际上是通过 application.yml 文件来配置限流器的。因此,上面的 rateLimiterConfigbackendARateLimiter Bean 是可选的,并且在这个例子中不会被使用。

4. 使用限流器注解

在你的控制器或服务类中使用 @RateLimiter 注解来应用限流器:

java 复制代码
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class MyController {

    @RateLimiter(name = "backendA")
    @GetMapping("/limited")
    public String limitedEndpoint() {
        return "请求成功!";
    }
}

5. 启动应用程序

现在,你可以启动你的 Spring Boot 应用程序,并通过浏览器或 API 测试工具发送请求到 /api/limited 端点来验证限流是否按预期工作。

当你超过配置的限流阈值时,Resilience4j 会自动返回一个 BlockedException,你可以通过自定义异常处理器来处理这个异常,并向客户端返回友好的错误信息。

6. 自定义异常处理(可选)

你可以创建一个全局异常处理器来捕获 BlockedException 并返回自定义的响应:

java 复制代码
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.ratelimiter.BlockedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BlockedException.class)
    public ResponseEntity<String> handleBlockedException(BlockedException ex, WebRequest request) {
        return new ResponseEntity<>("Too many requests - try again later", HttpStatus.TOO_MANY_REQUESTS);
    }

    // 你可以在这里添加其他异常处理器的定义
}

这样,当限流器阻止请求时,客户端将收到一个带有 429 Too Many Requests 状态码的响应。

四、使用Spring Boot中的过滤器实现API限流

在Spring Boot中,你可以通过创建一个自定义过滤器(Filter)来实现API限流。这种方法允许你在请求到达控制器之前进行拦截,并根据某些条件(如请求的IP地址、用户ID等)来决定是否允许请求继续处理。

以下是一个使用Spring Boot中的过滤器实现简单API限流的完整代码示例:

1. 引入依赖

确保你的pom.xml文件中包含了Spring Boot的Web依赖:

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 其他依赖项 -->
</dependencies>

2. 创建限流过滤器

创建一个实现javax.servlet.Filter接口的类,用于实现限流逻辑:

java 复制代码
import javax.servlet.*;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ApiRateLimiterFilter implements Filter {

    private static final int LIMIT = 10; // 限流阈值
    private static final int WINDOW_SIZE_SECONDS = 60; // 时间窗口大小
    private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Long> lastRequestTimes = new ConcurrentHashMap<>();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化逻辑(可选)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String clientId = getClientId(httpRequest); // 获取客户端ID(可以是IP地址、用户ID等)

            long currentTime = System.currentTimeMillis();
            Long lastRequestTime = lastRequestTimes.get(clientId);
            AtomicInteger count = requestCounts.computeIfAbsent(clientId, k -> new AtomicInteger(0));

            if (lastRequestTime == null || (currentTime - lastRequestTime) > TimeUnit.SECONDS.toMillis(WINDOW_SIZE_SECONDS)) {
                // 如果在时间窗口外,重置计数器
                requestCounts.put(clientId, new AtomicInteger(1));
                lastRequestTimes.put(clientId, currentTime);
            } else {
                // 如果在时间窗口内,增加计数器
                if (count.incrementAndGet() > LIMIT) {
                    // 如果超过限流阈值,拒绝请求
                    response.getWriter().write("Too many requests - try again later");
                    return;
                }
                // 更新最后请求时间
                lastRequestTimes.put(clientId, currentTime);
            }

            // 允许请求继续处理
            chain.doFilter(request, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
        // 清理逻辑(可选)
        requestCounts.clear();
        lastRequestTimes.clear();
    }

    // 获取客户端ID的方法(这里以IP地址为例)
    private String getClientId(HttpServletRequest request) {
        return request.getRemoteAddr();
    }
}

3. 注册过滤器

你需要将过滤器注册到Spring Boot应用程序中。这可以通过在配置类上添加@Component注解或使用FilterRegistrationBean来完成。

使用@Component注解
java 复制代码
import org.springframework.stereotype.Component;

@Component
public class ApiRateLimiterFilter extends ... {
    // 过滤器实现(如上所示)
}
使用FilterRegistrationBean

如果你不想在过滤器类上直接添加@Component注解,你可以在配置类中注册它:

java 复制代码
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<ApiRateLimiterFilter> loggingFilter(){
        FilterRegistrationBean<ApiRateLimiterFilter> registrationBean 
          = new FilterRegistrationBean<>();

        registrationBean.setFilter(new ApiRateLimiterFilter());
        registrationBean.addUrlPatterns("/api/*"); // 设置需要过滤的URL模式

        return registrationBean;    
    }
}

4. 测试限流

现在,你可以启动Spring Boot应用程序,并通过浏览器或API测试工具发送请求到你的API端点来验证限流是否按预期工作。

注意:上面的代码实现了一个简单的计数器限流算法,但它并没有考虑多线程环境下的并发问题。在生产环境中,你可能需要使用更复杂的限流算法(如令牌桶、漏桶算法)或结合Redis等分布式存储来实现更可靠的限流机制。此外,上面的代码示例没有实现持久化逻辑,因此在应用重启后,限流计数器会被重置。如果你需要持久化限流数据,你可能需要将其存储在数据库或其他持久化存储中。

相关推荐
FG.几秒前
SpringCloud
后端·spring·spring cloud
谢栋_2 分钟前
设计模式从入门到精通之(四)建造者模式
java·设计模式·建造者模式
Cikiss4 分钟前
Git 基础——《Pro Git》
git·后端·源代码管理
HelloZheQ13 分钟前
从用户输入 URL 到后端响应的完整流程解析
java
GGBondlctrl17 分钟前
【Spring Boot】Spring 事务探秘:核心机制与应用场景解析
java·spring·事务·spring事务·transaction·声明式事务·编程式事务
多多*18 分钟前
后端技术选型 sa-token校验学习 下 结合项目学习 前后端登录
java·redis·git·学习·github·intellij-idea·状态模式
Seven9724 分钟前
《深入理解Mybatis原理》Mybatis中的缓存实现原理
java·mybatis
黄名富27 分钟前
Kafka 主题管理
java·分布式·kafka
哥谭居民000133 分钟前
学技术步骤,(tomcat举例)jar包api手写tomcat静态资源基础服务器
java·服务器·tomcat
苹果酱056733 分钟前
React性能优化: 使用React.lazy与Suspense提高加载效率
java·vue.js·spring boot·mysql·课程设计