Guava、Resilience4j、Redis、Spring Cloud Gateway等多种方式实现限流,本文将详细介绍SpringBoot中如何实现限流。
chou403
/ SpringCloud
/ c:
/ u:
/ 13 min read
一学一个不吱声
在Spring Boot中实现限流可以通过多种方式,以下是几种常见的方法及其步骤:
一、使用Guava的RateLimiter(单机限流)
适用于单服务实例的限流,基于令牌桶算法。
步骤:
-
添加依赖:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0.1-jre</version> </dependency>
-
创建自定义注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimit { double permitsPerSecond(); // 每秒允许的请求数 }
-
实现AOP切面:
@Aspect @Component public class RateLimitAspect { private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>(); @Around("@annotation(rateLimit)") public Object limit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = joinPoint.getSignature().toLongString(); RateLimiter limiter = limiters.computeIfAbsent(key, k -> RateLimiter.create(rateLimit.permitsPerSecond())); if (limiter.tryAcquire()) { return joinPoint.proceed(); } else { throw new RuntimeException("请求过于频繁,请稍后再试"); } } }
-
使用注解:
@RestController public class ApiController { @GetMapping("/api") @RateLimit(permitsPerSecond = 1.0) public String api() { return "Success"; } }
-
全局异常处理:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntity<String> handleRateLimit(RuntimeException ex) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ex.getMessage()); } }
二、使用Resilience4j(单机/分布式支持)
Resilience4j提供更丰富的容错机制,支持Rate Limiter、Bulkhead等。
步骤:
-
添加依赖:
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.7.1</version> </dependency>
-
配置限流规则(
application.yml
):resilience4j: ratelimiter: instances: apiRateLimiter: limitForPeriod: 10 # 时间窗口内允许的请求数 limitRefreshPeriod: 1s # 时间窗口长度 timeoutDuration: 0 # 获取令牌的等待时间(0表示立即失败)
-
使用注解限流:
@RestController public class ApiController { @GetMapping("/api") @RateLimiter(name = "apiRateLimiter") public String api() { return "Success"; } }
-
自定义Fallback:
@ControllerAdvice public class RateLimiterFallback { @ExceptionHandler(RequestNotPermitted.class) public ResponseEntity<String> handleRateLimit() { return ResponseEntity.status(429).body("请求过多"); } }
三、使用Redis实现分布式限流
适用于多实例环境,确保全局限流。
步骤(使用Lua脚本保证原子性):
-
添加Redis依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
编写Lua脚本(
rate_limit.lua
):local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local current = redis.call('GET', key) or 0 if tonumber(current) >= limit then return 0 else redis.call('INCR', key) redis.call('EXPIRE', key, window) return 1 end
-
实现限流服务:
@Service public class RedisRateLimiter { @Autowired private StringRedisTemplate redisTemplate; private static final DefaultRedisScript<Long> RATE_LIMIT_SCRIPT; static { RATE_LIMIT_SCRIPT = new DefaultRedisScript<>(); RATE_LIMIT_SCRIPT.setLocation(new ClassPathResource("rate_limit.lua")); RATE_LIMIT_SCRIPT.setResultType(Long.class); } public boolean allow(String key, int limit, int window) { List<String> keys = Collections.singletonList(key); Long result = redisTemplate.execute(RATE_LIMIT_SCRIPT, keys, String.valueOf(limit), String.valueOf(window)); return result != null && result == 1; } }
-
在Controller中使用:
@RestController public class ApiController { @Autowired private RedisRateLimiter rateLimiter; @GetMapping("/api") public ResponseEntity<String> api(@RequestParam String userId) { String key = "rate_limit:api:" + userId; if (!rateLimiter.allow(key, 10, 60)) { // 每分钟允许10次 return ResponseEntity.status(429).body("请求过多"); } return ResponseEntity.ok("Success"); } }
四、使用Spring Cloud Gateway(网关层限流)
适用于微服务架构,在API网关层统一限流。
-
添加依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
-
配置路由规则(
application.yml
):spring: cloud: gateway: routes: - id: api_route uri: http://backend-service predicates: - Path=/api/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 # 每秒令牌数 redis-rate-limiter.burstCapacity: 20 # 令牌桶容量 key-resolver: "#{@userKeyResolver}" # 按用户限流
-
定义Key解析器:
@Bean public KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); }
五、选型建议
- 单机限流:Guava或Resilience4j,简单高效。
- 分布式限流:Redis+Lua脚本或Redisson。
- 网关层限流:Spring Cloud Gateway集成Redis。
根据具体场景选择合适的方案,并结合监控(如Prometheus)实时观察限流效果。
DeepSeek 的思考
嗯,我现在要在Spring Boot中实现限流,这个我之前没做过,得先了解一下怎么做。限流,应该就是控制接口的访问频率,防止服务器过载对吧。那常见的限流算法有哪些呢?记得有计数器、滑动窗口、漏桶、令牌桶这些算法。可能需要用其中的一种来实现。
首先,我需要确定用哪种算法。比如,Guava的RateLimiter用的是令牌桶算法,可能比较适合。或者用Spring Cloud Gateway里的限流,不过如果只是Spring Boot项目的话,可能不需要用到网关层面的限流。可能更倾向于应用层自己实现。
然后,实现的方式可能有几种。比如用AOP切面,在方法调用前进行限流判断。或者用拦截器,在请求处理前进行限流。或者用过滤器,在更早的阶段处理。另外,可能还可以结合注解,方便在需要的地方添加限流。
比如,用Guava的RateLimiter的话,可以创建一个切面,在方法上添加自定义注解,然后在切面里获取RateLimiter实例,调用acquire方法或者tryAcquire来判断是否有令牌,没有的话就抛异常或者返回错误信息。
不过需要考虑的是,RateLimiter是单机的限流,如果是分布式服务的话,可能得用Redis之类的中间件来实现分布式限流。比如用Redis的计数器,结合过期时间来实现固定窗口,或者用更复杂的Lua脚本来实现滑动窗口、令牌桶等算法。
比如固定窗口的话,可以用Redis的incr命令,每个时间窗口内计数,超过阈值就拒绝。但这样可能会有临界问题,比如时间窗口交界处流量突增。而滑动窗口可能更平滑,但实现复杂一些。或者用令牌桶,每次请求时获取令牌,Redis存储令牌数和上次更新时间,计算当前可用令牌数。
另外,可能还可以用现有的库,比如Spring的Spring Cloud Gateway有RequestRateLimiterGatewayFilterFactory,但可能只适用于网关层面。或者使用Resilience4j这个库,它提供了RateLimiter的功能,可以方便地集成到Spring Boot中。
比如,用Resilience4j的话,可以通过注解的方式在方法上添加限流规则,然后通过AOP来处理。或者直接使用它的RateLimiterRegistry来创建限流器,然后在代码中手动调用。
另外,还有Hystrix,不过好像Hystrix已经不太维护了,可能Resilience4j更好一些。
或者,使用Spring Boot Actuator的Metrics,结合Micrometer和Prometheus等监控系统来实现限流,不过这可能更偏向监控而非直接限流。
现在我需要具体步骤。假设使用Guava的RateLimiter来实现单机限流,可能步骤如下:
1. 添加Guava依赖到pom.xml中。
2. 创建一个自定义注解,比如@RateLimit,可以配置每秒的许可数。
3. 创建一个AOP切面,拦截带有@RateLimit注解的方法。
4. 在切面中,为每个方法或每个类维护一个RateLimiter实例,根据注解的参数初始化。
5. 在方法执行前调用rateLimiter.tryAcquire(),如果获取不到,抛出自定义异常。
6. 全局异常处理器捕获这个异常,返回429 Too Many Requests的响应。
这样就能在方法级别实现限流了。不过需要考虑RateLimiter的存储方式,比如使用ConcurrentHashMap来保存不同方法的RateLimiter实例,避免多线程问题。
或者,使用拦截器,针对某个API路径进行限流,比如每个用户IP限流。这时候需要根据IP来区分,存储每个IP的RateLimiter,但需要注意内存泄漏问题,可能需要定期清理不活跃的IP。
如果要做分布式限流,可能需要用Redis。例如,每个接口的请求次数存储在Redis中,每次请求时通过Lua脚本原子性地增加计数并判断是否超限。比如,用Redis的过期时间来实现时间窗口。
比如,一个简单的固定窗口限流方案:键名可以是接口名+时间窗口的起始时间,比如rate_limit:apiName:123456(时间戳)。每次请求时,获取当前时间窗口的起始时间,然后incr这个键,如果超过阈值就拒绝。同时设置过期时间为窗口大小,比如1分钟,这样自动清理。
不过固定窗口的问题是两个时间窗口交界处可能会有双倍流量。滑动窗口可能更准确,但实现复杂。或者用令牌桶算法,每次请求时计算当前可用令牌数,需要维护上次请求的时间和当前令牌数,这可以用Redis存储这两个值,用Lua脚本来保证原子性操作。
比如,每次请求执行以下步骤:
1. 获取当前时间。
2. 从Redis中取出上次填充令牌的时间(lastTime)和当前令牌数(tokens)。
3. 计算从lastTime到现在的时间差,转换成秒,然后计算这段时间内可以生成多少令牌,比如每秒生成rate个令牌。
4. 将令牌数增加到不超过桶的容量。
5. 如果令牌数大于等于1,扣除一个令牌,允许请求,并更新lastTime和tokens。
6. 否则,拒绝请求。
这个过程需要用Lua脚本来保证原子性,避免并发问题。
或者,使用现成的Redis库,比如Redisson,它提供了RRateLimiter接口,实现了基于Redis的分布式限流,支持令牌桶算法。
比如,使用Redisson的话,步骤如下:
1. 添加Redisson依赖。
2. 配置Redisson客户端连接到Redis。
3. 在需要限流的方法中,获取RRateLimiter实例,设置速率和容量。
4. 每次请求时调用tryAcquire方法,如果失败则拒绝请求。
这样就能实现分布式限流了。不过需要依赖Redisson,并且需要维护Redis的连接。
总结来说,Spring Boot中实现限流的方式有多种,选择哪种取决于需求:单机还是分布式,算法类型,以及是否需要简单的实现。
如果只是单机限流,使用Guava的RateLimiter配合AOP是比较快捷的方式。如果是分布式,可以考虑Redisson或者自己用Redis+Lua脚本实现。
另外,可能还要考虑限流的维度,比如全局限流、按用户、按IP等,这时候键的设计需要包含这些维度信息。
比如按IP限流的话,键可以是rate_limit:apiName:ipAddress,这样每个IP独立计数。
在实现时,还需要处理突发流量,比如令牌桶允许一定程度的突发,而漏桶则平滑流出。根据需求选择合适的算法。
最后,实现后需要测试,确认限流是否生效,比如使用Jmeter或者Postman发送大量请求,观察是否返回429状态码,以及限流后的处理是否合理,比如返回错误信息或者执行降级逻辑。