通过一个完整的Spring Boot项目演示如何用Redis实现简单的API限流功能。我们将从零开始搭建项目。
一、环境准备
1.1 开发环境要求
- JDK 1.8+
- IntelliJ IDEA(推荐)
- Redis 5.0+(本地安装)
- Postman(测试用)
1.2 创建SpringBoot项目
使用IDEA创建新项目,选择:
- Spring Web
- Spring Data Redis (Lettuce)
- Lombok
二、基础配置
2.1 添加Redis配置
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
2.2 Redis配置类
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
三、核心代码实现
3.1 令牌桶算法Lua脚本
在resources目录创建rate_limiter.lua
:
lua
-- 令牌桶算法实现
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local rate = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- 获取当前令牌数
local data = redis.call("HMGET", key, "tokens", "last_time")
local tokens = tonumber(data[1]) or capacity
local last_time = tonumber(data[2]) or now
-- 计算新增令牌
local delta = math.floor((now - last_time) * rate)
if delta > 0 then
tokens = math.min(tokens + delta, capacity)
last_time = now
end
-- 判断是否允许请求
local result = 0
if tokens >= requested then
tokens = tokens - requested
result = 1
end
-- 更新存储
redis.call("HMSET", key, "tokens", tokens, "last_time", last_time)
redis.call("EXPIRE", key, 86400)
return result
3.2 限流服务类
java
@Service
@RequiredArgsConstructor
public class RateLimitService {
private final RedisTemplate<String, Object> redisTemplate;
// 加载Lua脚本
private static final DefaultRedisScript<Long> REDIS_SCRIPT;
static {
REDIS_SCRIPT = new DefaultRedisScript<>();
REDIS_SCRIPT.setLocation(new ClassPathResource("rate_limiter.lua"));
REDIS_SCRIPT.setResultType(Long.class);
}
/**
* 尝试获取令牌
* @param key 限流key
* @param capacity 桶容量
* @param rate 令牌生成速率(个/秒)
* @param requested 请求令牌数
* @return 是否允许请求
*/
public boolean tryAcquire(String key, int capacity, double rate, int requested) {
Long result = redisTemplate.execute(
REDIS_SCRIPT,
Collections.singletonList(key),
Instant.now().getEpochSecond(),
capacity,
rate,
requested
);
return result != null && result == 1;
}
}
3.3 自定义限流注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default ""; // 限流key前缀
int capacity() default 100; // 桶容量
double rate() default 10.0; // 令牌生成速率
int permits() default 1; // 每次请求消耗令牌数
}
3.4 切面实现
java
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RateLimitService rateLimitService;
@Around("@annotation(rateLimit)")
public Object checkLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = buildKey(joinPoint, rateLimit);
boolean allow = rateLimitService.tryAcquire(
key,
rateLimit.capacity(),
rateLimit.rate(),
rateLimit.permits()
);
if (!allow) {
throw new RuntimeException("请求太频繁,请稍后再试!");
}
return joinPoint.proceed();
}
private String buildKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return "rate_limit:" +
(rateLimit.key().isEmpty() ?
signature.getMethod().getName() : rateLimit.key());
}
}
四、使用示例
4.1 创建测试Controller
java
@RestController
public class TestController {
@RateLimit(key = "testApi", capacity = 5, rate = 1)
@GetMapping("/test")
public String testApi() {
return "请求成功!当前时间:" + LocalDateTime.now();
}
}
4.2 异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleLimit(RuntimeException ex) {
return ResponseEntity.status(429).body(ex.getMessage());
}
}
五、测试验证
5.1 使用Postman测试
- 快速连续访问
http://localhost:8080/test
- 观察响应结果变化:
正常响应:
json
"请求成功!当前时间:2024-05-20T15:30:45.123"
限流响应:
json
{
"status": 429,
"message": "请求太频繁,请稍后再试!"
}
5.2 测试结果预期
请求次数 | 结果 |
---|---|
1-5 | 成功 |
6-10 | 部分失败 |
后续请求 | 每秒1次成功 |
六、常见问题解答
Q1:Redis宕机了怎么办?
A:可添加本地限流作为备用方案(推荐使用Guava的RateLimiter)
Q2:如何区分不同用户?
A:在key中加入用户ID:@RateLimit(key = "user_#{userId}")
Q3:如何修改限流规则?
A:直接修改注解参数即可立即生效
七、完整项目结构
rate-limiter-demo
├── src/main/java
│ └── com
│ └── example
│ ├── config/RedisConfig.java
│ ├── service/RateLimitService.java
│ ├── aspect/RateLimitAspect.java
│ ├── annotation/RateLimit.java
│ ├── controller/TestController.java
│ └── exception/GlobalExceptionHandler.java
├── src/main/resources
│ ├── rate_limiter.lua
│ └── application.yml