Spring AOP + Redisson 实现基于注解的分布式限流方案
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 依赖:
<!-- Redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.50.0</version>
</dependency>
4. 配置 Redis
在 application.yml
中配置 Redis 连接信息:
spring:data:redis:host: localhostport: 6379password:ttl: 3600database: 0
5. Redisson 客户端配置
使用 @Configuration
定义 RedissonClient
:
@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;@Beanpublic 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. 自定义注解
通过注解声明限流规则:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {// 限流key前缀String key() default "";// 每个时间窗口允许的请求数int rate() default 10;// 时间窗口(秒)int rateInterval() default 1;// 限流类型:API、USER、IPRateLimitType limitType() default RateLimitType.USER;// key过期时间(小时)int expireTime() default 1;// 提示信息String message() default "请求过于频繁,请稍后再试";
}
限流类型枚举:
public enum RateLimitType {API, // 接口级别USER, // 用户级别IP // IP级别
}
7. AOP 切面实现
使用切面在方法调用前执行限流逻辑:
@Aspect
@Component
@Slf4j
public class RateLimitAspect {@Resourceprivate RedissonClient redissonClient;@Resourceprivate UserService userService;@Before("@annotation(rateLimit)")public void doBefore(JoinPoint point, RateLimit rateLimit) {// 生成限流KeyString 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. 使用示例
在需要限流的接口上添加注解即可:
@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 的组合,我们可以在不改动业务逻辑的前提下,快速实现高性能、可扩展的分布式限流,让接口在高并发场景下更安全、更稳定。