SpringBoot实战:高效实现API限流策略
一、Guava库
在SpringBoot项目中实现限流,我们可以使用多种策略,如基于令牌桶(Token Bucket)的Guava RateLimiter,或者基于漏桶(Leaky Bucket)算法的自定义实现,甚至是基于Redis的分布式限流。以下是一个使用Guava RateLimiter在SpringBoot项目中实现限流的完整示例。
1. 引入依赖
首先,在pom.xml文件中添加Guava库的依赖:
<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注入到其他组件中:
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,并在请求处理方法中使用它:
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的依赖:
<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.properties或application.yml文件中配置Redis连接信息:
# application.properties spring.redis.host=localhost spring.redis.port=6379 # 如果Redis设置了密码,请取消以下行的注释并设置密码 # spring.redis.password=yourpassword或者
# application.yml spring: redis: host: localhost port: 6379 # 如果Redis设置了密码,请设置密码 # password: yourpassword3. 创建Redis限流服务
创建一个服务类,用于实现限流逻辑:
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请求并调用限流服务:
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 的依赖:
<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.yml或application.properties文件中配置 Resilience4j 限流器:
resilience4j.ratelimiter: instances: backendA: limitForPeriod: 10 # 在指定时间窗口内允许的请求数量 limitRefreshPeriod: 10s # 时间窗口大小 timeoutDuration: 0 # 可选,请求超时时间3. 创建限流配置类
创建一个配置类来定义和配置 Resilience4j 的限流器实例:
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"); } */ }注意:上面的RateLimiterConfig和RateLimiterRegistryBean 是为了展示如何通过代码配置 Resilience4j,但在这个例子中,我们实际上是通过application.yml文件来配置限流器的。因此,上面的rateLimiterConfig和backendARateLimiterBean 是可选的,并且在这个例子中不会被使用。
4. 使用限流器注解
在你的控制器或服务类中使用@RateLimiter注解来应用限流器:
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并返回自定义的响应:
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依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 其他依赖项 --> </dependencies>2. 创建限流过滤器
创建一个实现javax.servlet.Filter接口的类,用于实现限流逻辑:
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注解
import org.springframework.stereotype.Component; @Component public class ApiRateLimiterFilter extends ... { // 过滤器实现(如上所示) }使用FilterRegistrationBean
如果你不想在过滤器类上直接添加@Component注解,你可以在配置类中注册它:
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等分布式存储来实现更可靠的限流机制。此外,上面的代码示例没有实现持久化逻辑,因此在应用重启后,限流计数器会被重置。如果你需要持久化限流数据,你可能需要将其存储在数据库或其他持久化存储中。
