1. 背景与需求
在高并发系统中,为了防止接口被频繁调用导致服务器过载,我们通常需要对接口进行限流。常见的限流方案包括:
-
固定窗口 、滑动窗口 、令牌桶等算法
-
基于本地缓存(如
Guava RateLimiter
) -
基于分布式缓存(如 Redis + Lua 脚本)
本文将演示一种简单且高效的实现方案:
使用 Spring AOP + 自定义注解 + Redisson 分布式限流器 ,实现用户/IP/API 维度的注解式限流。
核心目标:
-
通过注解轻松配置接口的限流规则。
-
支持接口级别 、用户级别 、IP级别限流。
-
使用 Redisson 简化 Redis 分布式锁与限流操作。
2. 技术选型
技术 | 作用 |
---|---|
Spring AOP | 切面编程,拦截带有 @RateLimit 注解的方法 |
Redisson | 对 Redis 操作的高级封装,提供分布式限流器 RRateLimiter |
Redis | 存储限流状态,支持集群部署,天然分布式 |
3. 依赖引入
在 pom.xml
中添加 Redisson 依赖:
XML
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.50.0</version>
</dependency>
4. 配置 Redis
在 application.yml
中配置 Redis 连接信息:
XML
spring:
data:
redis:
host: localhost
port: 6379
password:
ttl: 3600
database: 0
5. Redisson 客户端配置
使用 @Configuration
定义 RedissonClient
:
java
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private Integer redisPort;
@Value("${spring.data.redis.password}")
private String redisPassword;
@Value("${spring.data.redis.database}")
private Integer redisDatabase;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + redisHost + ":" + redisPort;
SingleServerConfig singleServerConfig = config.useSingleServer()
.setAddress(address)
.setDatabase(redisDatabase)
.setConnectionMinimumIdleSize(1)
.setConnectionPoolSize(10)
.setIdleConnectionTimeout(30000)
.setConnectTimeout(5000)
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
// 如果有密码则设置密码
if (redisPassword != null && !redisPassword.isEmpty()) {
singleServerConfig.setPassword(redisPassword);
}
return Redisson.create(config);
}
}
6. 自定义注解
通过注解声明限流规则:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
// 限流key前缀
String key() default "";
// 每个时间窗口允许的请求数
int rate() default 10;
// 时间窗口(秒)
int rateInterval() default 1;
// 限流类型:API、USER、IP
RateLimitType limitType() default RateLimitType.USER;
// key过期时间(小时)
int expireTime() default 1;
// 提示信息
String message() default "请求过于频繁,请稍后再试";
}
限流类型枚举:
java
public enum RateLimitType {
API, // 接口级别
USER, // 用户级别
IP // IP级别
}
7. AOP 切面实现
使用切面在方法调用前执行限流逻辑:
java
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Resource
private RedissonClient redissonClient;
@Resource
private UserService userService;
@Before("@annotation(rateLimit)")
public void doBefore(JoinPoint point, RateLimit rateLimit) {
// 生成限流Key
String key = generateRateLimitKey(point, rateLimit);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.expire(Duration.ofHours(rateLimit.expireTime()));
// 设置限流参数:时间窗口内允许请求数
rateLimiter.trySetRate(RateType.OVERALL, rateLimit.rate(), rateLimit.rateInterval(), RateIntervalUnit.SECONDS);
// 尝试获取令牌
if (!rateLimiter.tryAcquire(1)) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST, rateLimit.message());
}
}
private String generateRateLimitKey(JoinPoint point, RateLimit rateLimit) {
StringBuilder keyBuilder = new StringBuilder("rate_limit:");
if (!rateLimit.key().isEmpty()) {
keyBuilder.append(rateLimit.key()).append(":");
}
switch (rateLimit.limitType()) {
case API:
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
keyBuilder.append("api:").append(method.getDeclaringClass().getSimpleName())
.append(".").append(method.getName());
break;
case USER:
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
User loginUser = userService.getLoginUser(request);
keyBuilder.append("user:").append(loginUser.getId());
} else {
keyBuilder.append("ip:").append(getClientIP());
}
} catch (BusinessException e) {
keyBuilder.append("ip:").append(getClientIP());
}
break;
case IP:
keyBuilder.append("ip:").append(getClientIP());
break;
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的限流类型");
}
return keyBuilder.toString();
}
private String getClientIP() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return "unknown";
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
log.info("客户端IP: {}", ip);
return ip != null ? ip : "unknown";
}
}
8. 使用示例
在需要限流的接口上添加注解即可:
java
@RestController
@RequestMapping("/api/test")
public class TestController {
// 用户级别限流:每秒最多 5 次
@RateLimit(rate = 5, rateInterval = 1, limitType = RateLimitType.USER, message = "操作太频繁啦,请稍后重试!")
@GetMapping("/userLimit")
public String userLimit() {
return "用户级别限流测试成功";
}
// IP级别限流:每分钟 20 次
@RateLimit(rate = 20, rateInterval = 60, limitType = RateLimitType.IP)
@GetMapping("/ipLimit")
public String ipLimit() {
return "IP级别限流测试成功";
}
// 接口级别限流:每秒 10 次
@RateLimit(rate = 10, rateInterval = 1, limitType = RateLimitType.API)
@GetMapping("/apiLimit")
public String apiLimit() {
return "接口级别限流测试成功";
}
}
9. 工作流程
-
注解声明规则 :开发者在 Controller 方法上标记
@RateLimit
,指定key前缀、速率、时间窗口、key过期时间和限流类型等(未指定将使用默认值)。 -
AOP 拦截请求:切面拦截注解方法,在方法执行前通过 Redisson 进行令牌检查。
-
Redis 记录状态:Redisson 将限流数据存入 Redis,实现分布式共享。
-
限流生效:若未获取到令牌,直接抛出自定义异常并返回提示信息。
10. 总结
-
优点:
-
使用注解即可快速启用限流,低侵入。
-
Redisson 提供分布式保证,适用于集群环境。
-
支持多种维度(API、用户、IP)灵活配置。
-
-
适用场景:
-
接口防刷、防爬虫
-
高频操作(如短信发送、验证码请求)
-
防止恶意用户短时间内发起过多请求
-
这套方案简单易用,可作为分布式系统中的通用限流基础组件。
若想更精细的控制限流规则请参考另几篇文章:
https://blog.csdn.net/xundefined/article/details/144200495
https://blog.csdn.net/xundefined/article/details/144300779
通过 Spring AOP + Redisson 的组合,我们可以在不改动业务逻辑的前提下,快速实现高性能、可扩展的分布式限流,让接口在高并发场景下更安全、更稳定。