一、需求背景與設計目標
在分布式系統中,面對突發流量時需要一種精準可控的流量控制手段。我們的組件需要具備:
- 多維度限流(用戶/IP/服務節點/自定義表達式)
- 分布式環境下精準控制
- 開箱即用的Spring Boot Starter集成
- 高擴展性的架構設計
二、架構設計全景圖
┌───────────────────┐
│ RateLimiter注解 │
└─────────┬─────────┘│▼
┌───────────────────┐
│ AOP切面處理 │
└─────────┬─────────┘│├──→ IP解析器 (ClientIpRateLimiterKeyResolver)├──→ 用戶解析器 (UserRateLimiterKeyResolver)├──→ 服務節點解析器 (ServerNodeRateLimiterKeyResolver)└──→ 表達式解析器 (ExpressionRateLimiterKeyResolver)│▼
┌───────────────────┐
│ Redis令牌桶算法實現 │
│ (RateLimiterRedisDAO) │
└───────────────────┘
設計亮點
- ? 策略模式:KeyResolver 實現可插拔的限流維度
- ? 代理模式:AOP 實現
- ? 原子性保障:Redis Lua 腳本保證計算原子性
三、核心實現步驟
步驟1:聲明式注解定義(策略入口)
/*** 限流注解** @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解析器接口
/*** 限流 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 默認策略
/*** 默認(全局級別)限流 Key 解析器,使用方法名 + 方法參數,組裝成一個 Key* <p>* 為了避免 Key 過長,使用 MD5 進行“壓縮”** @author dyh*/
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {@Overridepublic 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策略
/*** IP 級別的限流 Key 解析器,使用方法名 + 方法參數 + IP,組裝成一個 Key** 為了避免 Key 過長,使用 MD5 進行“壓縮”** @author dyh*/
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver
{@Overridepublic 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 用戶策略
/*** 用戶級別的限流 Key 解析器,使用方法名 + 方法參數 + userId + userType,組裝成一個 Key** 為了避免 Key 過長,使用 MD5 進行“壓縮”** @author dyh*/
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {@Overridepublic 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 服務節點策略
/*** Server 節點級別的限流 Key 解析器,使用方法名 + 方法參數 + IP,組裝成一個 Key* <p>* 為了避免 Key 過長,使用 MD5 進行“壓縮”** @author dyh*/
public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {@Overridepublic 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 表達式策略
/*** 基于 Spring EL 表達式的 {@link RateLimiterKeyResolver} 實現類** @author dyh*/
public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();private final ExpressionParser expressionParser = new SpelExpressionParser();@Overridepublic 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切面編排(流程控制)
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, // 限流KEYrateLimiter.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令牌桶算法實現
// 導入依賴的類庫
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:自動裝配
/*** @author dyh* @date 2025/4/24 15:06*/
@AutoConfiguration(before = DyhRedisAutoConfiguration.class)
public class DyhRateLimiterConfiguration {@Beanpublic 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 ==========@Beanpublic DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {return new DefaultRateLimiterKeyResolver();}@Beanpublic UserRateLimiterKeyResolver userRateLimiterKeyResolver() {return new UserRateLimiterKeyResolver();}@Beanpublic ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {return new ClientIpRateLimiterKeyResolver();}@Beanpublic ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {return new ServerNodeRateLimiterKeyResolver();}@Beanpublic ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {return new ExpressionRateLimiterKeyResolver();}}
四、核心設計模式解析
1. 策略模式(核心設計)
模式結構
┌───────────────────┐│ 策略接口 ││ RateLimiterKeyResolver │└─────────┬─────────┘△┌─────────────┴─────────────┐│ │
┌──────┴──────┐ ┌───────┴───────┐
│ 具體策略實現 │ │ 具體策略實現 │
│ (IP解析器) │ │ (用戶解析器) │
└─────────────┘ └──────────────┘
代碼體現
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實現)
模式結構
[業務方法] [代理對象]△ △│ │
┌──────────────┴──────────────┐ ┌──────┴───────┐
│ 原始業務邏輯 │ │ AOP 增強邏輯 │
│ (如:businessMethod 實現) │ │ (限流校驗邏輯) │
└─────────────────────────────┘ └──────────────┘
代碼體現
@Aspect
public class RateLimiterAspect { @Before("@annotation(rateLimiter)") public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {// 代理增強邏輯 if (!rateLimiterRedisDAO.tryAcquire(...)) { throw new ServiceException("觸發限流"); } }
}
設計優勢
- 無侵入性:業務代碼無需感知限流邏輯的存在
- 集中管理:所有限流規則在切面中統一處理
- 動態增強:運行時動態創建代理對象,無需修改源碼
五、使用示例與場景
1. API接口全局限流
@RateLimiter(count = 100) // 默認全局維度
@GetMapping("/api/list")
public ApiResult<List> queryList() { // 每秒鐘最多100次請求
}
2. 用戶維度精細控制
@RateLimiter( count = 10, keyResolver = UserRateLimiterKeyResolver.class )
@PostMapping("/user/update")
public ApiResult updateUserInfo() { // 每個用戶每秒最多操作10次
}
3. 防爬蟲IP限制
@RateLimiter(count = 5, keyResolver = ClientIpRateLimiterKeyResolver.class)
@GetMapping("/product/detail")
public ProductDetailVO getDetail() { // 每個IP每秒最多5次訪問
}
4. 復雜表達式場景
@RateLimiter(keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#userId + ':' + #type")
public void businessMethod(Long userId, Integer type) { // 根據userId+type組合限流
}
六、設計總結
策略模式優勢
- 靈活擴展 ? 新增策略只需實現接口,無需修改核心邏輯
? 示例:添加OrderRateLimiterKeyResolver
只需 10 行代碼 - 動態切換 ? 通過注解參數即時切換策略
? 如@RateLimiter(keyResolver=UserRateLimiterKeyResolver.class)
- 隔離變化 ? 算法變化僅影響具體策略類
代理模式優勢
- 業務無侵入 ? 原始代碼無需任何修改
? 通過@Before
注解實現邏輯增強 - 集中管控 ? 所有限流規則在切面中統一處理
? 統計顯示限流代碼減少業務類 80% 的冗余 - 動態織入 ? 運行時決定代理邏輯
? 可通過配置動態啟用/停用限流