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等分布式存储来实现更可靠的限流机制。此外,上面的代码示例没有实现持久化逻辑,因此在应用重启后,限流计数器会被重置。如果你需要持久化限流数据,你可能需要将其存储在数据库或其他持久化存储中。

相关推荐
一只叫煤球的猫9 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz96510 小时前
tcp/ip 中的多路复用
后端
bobz96510 小时前
tls ingress 简单记录
后端
皮皮林55111 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友11 小时前
什么是OpenSSL
后端·安全·程序员
bobz96511 小时前
mcp 直接操作浏览器
后端
前端小张同学14 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook14 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康15 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在15 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net