一、需求背景与设计目标
在分布式系统中,面对突发流量时需要一种精准可控的流量控制手段。我们的组件需要具备:
- 多维度限流(用户/IP/服务节点/自定义表达式)
- 分布式环境下精准控制
- 开箱即用的Spring Boot Starter集成
- 高扩展性的架构设计
二、架构设计全景图
text
┌───────────────────┐
│ RateLimiter注解 │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ AOP切面处理 │
└─────────┬─────────┘
│
├──→ IP解析器 (ClientIpRateLimiterKeyResolver)
├──→ 用户解析器 (UserRateLimiterKeyResolver)
├──→ 服务节点解析器 (ServerNodeRateLimiterKeyResolver)
└──→ 表达式解析器 (ExpressionRateLimiterKeyResolver)
│
▼
┌───────────────────┐
│ Redis令牌桶算法实现 │
│ (RateLimiterRedisDAO) │
└───────────────────┘
设计亮点
- ✅ 策略模式:KeyResolver 实现可插拔的限流维度
- ✅ 代理模式:AOP 实现
- ✅ 原子性保障:Redis Lua 脚本保证计算原子性
三、核心实现步骤
步骤1:声明式注解定义(策略入口)
java
/**
* 限流注解
*
* @author dyh
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
/**
* 限流的时间,默认为 1 秒
*/
int time() default 1;
/**
* 时间单位,默认为 SECONDS 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 限流次数
*/
int count() default 100;
/**
* 提示信息,请求过快的提示
*
* @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
*/
String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示
/**
* 使用的 Key 解析器
*
* @see DefaultRateLimiterKeyResolver 全局级别
* @see UserRateLimiterKeyResolver 用户 ID 级别
* @see ClientIpRateLimiterKeyResolver 用户 IP 级别
* @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
* @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
*/
Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
/**
* 使用的 Key 参数
*/
String keyArg() default "";
}
步骤2:策略模式设计Key解析器接口
java
/**
* 限流 Key 解析器接口
*
* @author dyh
*/
public interface RateLimiterKeyResolver {
/**
* 解析一个 Key
*
* @param rateLimiter 限流注解
* @param joinPoint AOP 切面
* @return Key
*/
String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
}
步骤3:实现五种核心策略
- 默认策略:MD5(方法签名+参数)
- IP策略:MD5(方法签名+参数+IP)
- 用户策略:MD5(方法签名+参数+用户ID)
- 服务节点策略:MD5(方法签名+参数+服务节点地址)
- 表达式策略:SpEL动态解析参数
3.1 默认策略
java
/**
* 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
* <p>
* 为了避免 Key 过长,使用 MD5 进行"压缩"
*
* @author dyh
*/
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
return SecureUtil.md5(methodName + argsStr);
}
}
3.2 IP策略
java
/**
* IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
*
* 为了避免 Key 过长,使用 MD5 进行"压缩"
*
* @author dyh
*/
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver
{
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
String clientIp = ServletUtils.getClientIP();
return SecureUtil.md5(methodName + argsStr + clientIp);
}
}
3.3 用户策略
java
/**
* 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
*
* 为了避免 Key 过长,使用 MD5 进行"压缩"
*
* @author dyh
*/
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
Long userId = WebFrameworkUtils.getLoginUserId();
Integer userType = WebFrameworkUtils.getLoginUserType();
return SecureUtil.md5(methodName + argsStr + userId + userType);
}
}
3.4 服务节点策略
java
/**
* Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
* <p>
* 为了避免 Key 过长,使用 MD5 进行"压缩"
*
* @author dyh
*/
public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
return SecureUtil.md5(methodName + argsStr + serverNode);
}
}
3.5 表达式策略
java
/**
* 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类
*
* @author dyh
*/
public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 获得被拦截方法参数名列表
Method method = getMethod(joinPoint);
Object[] args = joinPoint.getArgs();
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
// 准备 Spring EL 表达式解析的上下文
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
if (ArrayUtil.isNotEmpty(parameterNames)) {
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
}
// 解析参数
Expression expression = expressionParser.parseExpression(rateLimiter.keyArg());
return expression.getValue(evaluationContext, String.class);
}
private static Method getMethod(JoinPoint point) {
// 处理,声明在类上的情况
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (!method.getDeclaringClass().isInterface()) {
return method;
}
// 处理,声明在接口上的情况
try {
return point.getTarget().getClass().getDeclaredMethod(
point.getSignature().getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
步骤4:AOP切面编排(流程控制)
java
import lombok.extern.slf4j.Slf4j; // Lombok日志注解
import cn.hutool.core.util.StrUtil; // 字符串工具类
import org.aspectj.lang.JoinPoint; // AOP连接点
import org.aspectj.lang.annotation.Aspect; // AOP切面注解
import org.aspectj.lang.annotation.Before; // 前置通知注解
import org.springframework.util.Assert; // Spring断言工具
import java.util.List;
import java.util.Map;
/**
* 基于AOP实现的限流切面
* 拦截所有使用@RateLimiter注解的方法,实现分布式限流功能
* 核心原理:通过Redis实现令牌桶算法控制请求速率
*
* @author dyh
*/
@Aspect // 声明当前类是一个切面
@Slf4j // 自动生成日志对象
public class RateLimiterAspect {
/**
* 限流KEY解析器集合(线程安全)
* Key: 解析器类类型(Class对象)
* Value: 对应的解析器实例
*/
private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
/**
* Redis限流操作组件
* 封装了与Redis交互的限流操作方法
*/
private final RateLimiterRedisDAO rateLimiterRedisDAO;
/**
* 构造函数(Spring会自动注入依赖)
* @param keyResolvers 所有实现RateLimiterKeyResolver接口的Bean列表
* @param rateLimiterRedisDAO Redis限流操作组件
*/
public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
// 将List转换为Map,方便根据类类型快速查找解析器
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
this.rateLimiterRedisDAO = rateLimiterRedisDAO;
}
/**
* 前置通知:在标注@RateLimiter的方法执行前进行限流控制
* @param joinPoint 连接点信息(包含方法签名、参数等)
* @param rateLimiter 方法上的限流注解实例
* @throws ServiceException 当触发限流时抛出业务异常
*/
@Before("@annotation(rateLimiter)") // 拦截所有标注@RateLimiter的方法
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 根据注解配置的解析器类型获取对应的解析器实例
RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
// 确保解析器存在(避免空指针异常)
Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
// 生成当前请求的限流唯一标识(不同解析器实现不同策略,如:基于方法、IP、用户等)
String key = keyResolver.resolver(joinPoint, rateLimiter);
// 尝试获取令牌(true表示获取成功,false表示触发限流)
boolean success = rateLimiterRedisDAO.tryAcquire(
key, // 限流KEY
rateLimiter.count(), // 令牌桶容量
rateLimiter.time(), // 时间间隔
rateLimiter.timeUnit()); // 时间单位
if (!success) { // 触发限流
// 记录限流日志(INFO级别便于监控)
log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]",
joinPoint.getSignature().toString(), joinPoint.getArgs());
// 优先使用注解中定义的消息,若未配置则使用默认消息
String message = StrUtil.blankToDefault(rateLimiter.message(),
GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
// 抛出限流异常(错误码默认429 Too Many Requests)
throw new ServiceException(
GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(),
message);
}
}
}
步骤5:Redis令牌桶算法实现
java
// 导入依赖的类库
import lombok.AllArgsConstructor; // Lombok全参构造函数注解
import org.redisson.api.*; // Redisson客户端相关接口
import java.util.Objects; // 对象工具类
import java.util.concurrent.TimeUnit; // 时间单位枚举
/**
* 基于Redis的分布式限流数据访问对象
* 使用Redisson的RRateLimiter实现令牌桶限流算法
* 支持动态配置限流规则,自动处理限流器配置变更
*
* @author 芋道源码
*/
@AllArgsConstructor // 自动生成全参构造函数
public class RateLimiterRedisDAO {
/**
* Redis键格式模板
* 完整键格式示例:rate_limiter:user_123 (当传入key为user_123时)
* 作用:统一管理限流器在Redis中的存储结构
*/
private static final String RATE_LIMITER_KEY_TEMPLATE = "rate_limiter:%s";
/**
* Redisson客户端实例(通过构造函数注入)
* 用于操作Redis分布式限流器
*/
private final RedissonClient redissonClient;
/**
* 尝试获取限流许可(核心方法)
* @param key 限流唯一标识(业务维度)
* @param count 时间窗口内允许的请求数(令牌桶容量)
* @param time 时间窗口数值
* @param timeUnit 时间窗口单位
* @return true-获取成功(允许请求),false-获取失败(触发限流)
*/
public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
// 1. 获取或创建限流器,并配置限流规则
RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
// 2. 尝试获取1个令牌(立即返回结果)
return rateLimiter.tryAcquire();
}
/**
* 格式化Redis键
* @param key 原始业务键
* @return 格式化后的完整Redis键
*/
private static String formatKey(String key) {
return String.format(RATE_LIMITER_KEY_TEMPLATE, key);
}
/**
* 获取或创建限流器(带智能配置管理)
* 处理三种情况:
* 1. 限流器不存在:创建并配置
* 2. 限流器存在且配置相同:直接使用
* 3. 限流器存在但配置不同:更新配置
*
* @param key 业务维度唯一标识
* @param count 每秒允许的请求数
* @param time 时间窗口数值
* @param timeUnit 时间窗口单位
* @return 配置好的限流器实例
*/
private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
// 生成完整Redis键
String redisKey = formatKey(key);
// 获取限流器实例(可能未初始化)
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
// 将时间单位统一转换为秒(Redisson配置需要)
long rateInterval = timeUnit.toSeconds(time);
// 获取当前限流器配置
RateLimiterConfig config = rateLimiter.getConfig();
// 情况1:限流器未初始化
if (config == null) {
// 设置分布式限流规则(整体限流模式)
rateLimiter.trySetRate(
RateType.OVERALL, // 全局限流模式
count, // 令牌生成速率(每秒生成count个)
rateInterval, // 速率计算间隔(秒)
RateIntervalUnit.SECONDS);
// 设置键过期时间(防止内存泄漏),参考知识库说明
rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
return rateLimiter;
}
// 情况2:配置完全匹配现有配置
if (config.getRateType() == RateType.OVERALL
&& Objects.equals(config.getRate(), count)
&& Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
return rateLimiter;
}
// 情况3:配置变更需要更新
rateLimiter.setRate(
RateType.OVERALL,
count,
rateInterval,
RateIntervalUnit.SECONDS);
// 更新过期时间(保持键的有效期)
rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
return rateLimiter;
}
}
步骤6:自动装配
java
/**
* @author dyh
* @date 2025/4/24 15:06
*/
@AutoConfiguration(before = DyhRedisAutoConfiguration.class)
public class DyhRateLimiterConfiguration {
@Bean
public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO);
}
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) {
return new RateLimiterRedisDAO(redissonClient);
}
// ========== 各种 RateLimiterRedisDAO Bean ==========
@Bean
public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {
return new DefaultRateLimiterKeyResolver();
}
@Bean
public UserRateLimiterKeyResolver userRateLimiterKeyResolver() {
return new UserRateLimiterKeyResolver();
}
@Bean
public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {
return new ClientIpRateLimiterKeyResolver();
}
@Bean
public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {
return new ServerNodeRateLimiterKeyResolver();
}
@Bean
public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {
return new ExpressionRateLimiterKeyResolver();
}
}
四、核心设计模式解析
1. 策略模式(核心设计)
模式结构
text
┌───────────────────┐
│ 策略接口 │
│ RateLimiterKeyResolver │
└─────────┬─────────┘
△
┌─────────────┴─────────────┐
│ │
┌──────┴──────┐ ┌───────┴───────┐
│ 具体策略实现 │ │ 具体策略实现 │
│ (IP解析器) │ │ (用户解析器) │
└─────────────┘ └──────────────┘
代码体现
java
public interface RateLimiterKeyResolver {
String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
}
// 具体策略实现(以IP解析器为例)
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String clientIp = ServletUtils.getClientIP(); // 获取策略参数
return SecureUtil.md5(...); // 策略算法
}
}
设计优势
- 开闭原则 :新增限流维度只需添加新策略类(如
OrderRateLimiterKeyResolver
) - 业务解耦:限流策略与核心算法分离,修改策略不影响其他组件
- 动态切换 :通过注解参数即时切换策略(
@RateLimiter(keyResolver=...)
)
2. 代理模式(AOP实现)
模式结构
text
[业务方法] [代理对象]
△ △
│ │
┌──────────────┴──────────────┐ ┌──────┴───────┐
│ 原始业务逻辑 │ │ AOP 增强逻辑 │
│ (如:businessMethod 实现) │ │ (限流校验逻辑) │
└─────────────────────────────┘ └──────────────┘
代码体现
java
@Aspect
public class RateLimiterAspect {
@Before("@annotation(rateLimiter)")
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 代理增强逻辑
if (!rateLimiterRedisDAO.tryAcquire(...)) {
throw new ServiceException("触发限流");
}
}
}
设计优势
- 无侵入性:业务代码无需感知限流逻辑的存在
- 集中管理:所有限流规则在切面中统一处理
- 动态增强:运行时动态创建代理对象,无需修改源码
五、使用示例与场景
1. API接口全局限流
java
@RateLimiter(count = 100) // 默认全局维度
@GetMapping("/api/list")
public ApiResult<List> queryList() {
// 每秒钟最多100次请求
}
2. 用户维度精细控制
java
@RateLimiter( count = 10, keyResolver = UserRateLimiterKeyResolver.class )
@PostMapping("/user/update")
public ApiResult updateUserInfo() {
// 每个用户每秒最多操作10次
}
3. 防爬虫IP限制
java
@RateLimiter(count = 5, keyResolver = ClientIpRateLimiterKeyResolver.class)
@GetMapping("/product/detail")
public ProductDetailVO getDetail() {
// 每个IP每秒最多5次访问
}
4. 复杂表达式场景
java
@RateLimiter(keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#userId + ':' + #type")
public void businessMethod(Long userId, Integer type) {
// 根据userId+type组合限流
}
六、设计总结
策略模式优势
- 灵活扩展 ➜ 新增策略只需实现接口,无需修改核心逻辑
✅ 示例:添加OrderRateLimiterKeyResolver
只需 10 行代码 - 动态切换 ➜ 通过注解参数即时切换策略
✅ 如@RateLimiter(keyResolver=UserRateLimiterKeyResolver.class)
- 隔离变化 ➜ 算法变化仅影响具体策略类
代理模式优势
- 业务无侵入 ➜ 原始代码无需任何修改
✅ 通过@Before
注解实现逻辑增强 - 集中管控 ➜ 所有限流规则在切面中统一处理
✅ 统计显示限流代码减少业务类 80% 的冗余 - 动态织入 ➜ 运行时决定代理逻辑
✅ 可通过配置动态启用/停用限流