Spring Boot 实现限流功能的代码示例与配置管理
1. Guava RateLimiter 的简单实现
Guava 提供了一个轻量级的限流工具类 RateLimiter
,适用于单机环境下的限流需求。以下是基于 Guava 的限流代码示例:
springboot系类代码:spring-boot-limit
import com.google.common.util.concurrent.RateLimiter;
@RestController
public class LimiterController {
private static final double QPS = 5; // 每秒最多允许请求次数
private final RateLimiter rateLimiter = RateLimiter.create(QPS);
@GetMapping("/limited-resource")
public String limitedResource() {
if (!rateLimiter.tryAcquire()) {
// 尝试获取令牌
throw new RuntimeException("Too many requests, please try again later.");
}
return "Access granted!";
}
}
此方法适合小型项目或单体应用中的限流场景。
2. Redis + Lua 脚本实现分布式限流
对于分布式系统而言,使用 Redis 结合 Lua 脚本可以有效保证限流的一致性。以下是一个完整的实现案例:
(a) 初始化项目依赖
在项目的 pom.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-aop</artifactId>
</dependency>
(b) 创建限流注解
定义一个自定义注解用于标记需要限流的方法:
@Target({
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int maxRequests(); // 最大请求数
long timeWindowInSeconds(); // 时间窗口长度(单位:秒)
}
© 编写 Lua 脚本
创建一个 Lua 脚本来执行滑动窗口算法:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
-- 获取当前时间戳
local now = redis.call('time')
now = tonumber(now[1]) * 1000 + math.floor(now[2] / 1000)
-- 删除过期记录
redis.call('zremrangebyscore', key, '-inf', tostring(now - window))
-- 添加新条目
redis.call('zadd', key, now, now)
-- 计算总数并清理多余数据
local count = tonumber(redis.call('zcard', key))
if count > limit then
return 0 -- 超出限制
else
redis.call('expire', key, window) -- 设置键的有效期
return 1 -- 请求成功
end
(d) 构建 Redis 处理器
封装 Redis 操作逻辑以便于调用:
@Component
public class RedisRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean isAllowed(String key, int maxRequests, long timeWindowInSeconds) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource("scripts/ratelimit.lua")));
script.setResultType(Long.class);
List<String> keys = Collections.singletonList(key);
Long result = redisTemplate.execute(script, keys,
String.valueOf(maxRequests), String.valueOf(timeWindowInSeconds));
return result != null && result == 1;
}
}
(e) 利用 AOP 实现拦截
编写切面类来拦截带有 @RateLimit
注解的方法:
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RedisRateLimiter redisRateLimiter;
@Around("@annotation(rateLimit)")
public Object handleRateLimitedMethod(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = generateKey(joinPoint); // 根据方法签名生成唯一 Key
if (!redisRateLimiter.isAllowed(key, rateLimit.maxRequests(), rateLimit.timeWindowInSeconds())) {
throw new RuntimeException(