在高并發場景下,接口限流是保障系統穩定性的重要手段。常見的限流算法有漏桶算法、令牌桶算法等,而單機模式的限流方案在分布式集群環境下往往失效。本文將介紹如何利用 Redisson 結合 Redis 實現分布式環境下的接口限流,確保集群中所有節點的流量控制保持一致。
分布式限流的核心挑戰
在單機系統中,我們可以通過本地緩存(如 Guava 的 RateLimiter)實現限流,但在分布式集群環境下,這種方案會遇到兩個核心問題:
- 集群節點間的限流狀態不共享,導致整體流量超過預期閾值
- 無法保證同一用戶 / IP 的請求在不同節點上被統一限制
因此,分布式限流需要一個「中心化的狀態存儲」來記錄流量數據,而 Redis 憑借其高并發特性和分布式特性,成為了理想的選擇。
基于 Redisson 的分布式限流設計思路
核心原理是通過 Redis 記錄每個用戶對接口的訪問頻率,利用分布式鎖實現并發控制,具體設計如下:
-
唯一標識用戶與接口
為了避免限制 A 用戶時影響 B 用戶,需要為每個用戶 + 接口組合生成唯一的「限流鍵」。一般為用戶:使用
token + 接口路徑
+用戶的id -
基于 Redis 的訪問頻率記錄
每次請求到來時,通過 Redisson 操作 Redis 記錄訪問時間,并檢查單位時間內的訪問次數是否超過閾值。 -
AOP 無侵入式攔截
通過自定義注解 + Spring AOP 攔截需要限流的接口,在請求到達時執行限流邏輯,不侵入業務代碼。 -
自動過期的限流狀態
為 Redis 中的限流鍵設置過期時間,避免長期存儲無效數據,同時確保超過限制時間后自動允許用戶再次訪問。
實現步驟
引入依賴
在 pom.xml
中添加 Redisson 和 AOP 依賴
<!-- Redisson 分布式工具 -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.3</version>
</dependency><!-- Spring AOP -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定義限流注解
創建 @NoRepeatSubmit
注解,用于標記需要限流的接口,并支持自定義限流參數:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {/*** 設置請求鎖定時間(秒)*/int lockTime() default 5;
}
實現限流切面
通過 AOP 攔截 @NoRepeatSubmit
注解的方法,使用 Redisson 操作 Redis 實現限流邏輯:
Aspect
@Component
public class RepeatSubmitAspect {private static final Logger log = LoggerFactory.getLogger(RepeatSubmitAspect.class);@Resourceprivate RedissonClient redissonClient;@Pointcut("@annotation(com.example.demo.config.NoRepeatSubmit)")public void pointCut() {}@Around("pointCut()")public Object around(ProceedingJoinPoint pjp) throws Throwable {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {throw new IllegalArgumentException("無法獲取請求信息");}HttpServletRequest request = attributes.getRequest();String token = request.getHeader("token");String path = request.getServletPath();if (token == null || token.isEmpty()) {throw new IllegalArgumentException("缺少token請求頭");}// 使用token+path作為鎖的keyString key = "repeat_submit:" + token + ":" + path;RLock lock = redissonClient.getLock(key);// 嘗試獲取鎖,等待0秒,自動釋放時間由注解指定boolean isSuccess = false;isSuccess = lock.tryLock(0, annotation.lockTime(), TimeUnit.SECONDS);if (isSuccess) {log.info("獲取鎖成功: {}", key);// 執行目標方法return pjp.proceed();} else {log.info("重復請求,獲取鎖失敗: {}", key);return Result.fail("請勿重復提交請求");}}
}
測試
@RestController
@RequestMapping("/api/order")
public class OrderController {@PostMapping("/create")@NoRepeatSubmit(lockTime = 10) // 設置5秒內不允許重復提交public Result createOrder() {// 模擬訂單創建過程try {Thread.sleep(2000); // 模擬業務處理耗時2秒} catch (InterruptedException e) {e.printStackTrace();}return Result.success("訂單創建成功");}
}
限制之后
這里還可以增加更多的邏輯,比如限制次數等等。
核心邏輯說明
- 用戶唯一標識生成
通過getUniqueUserKey
方法獲取用戶標識:已登錄用戶用token
,未登錄用戶用IP
,確保不同用戶的限流互不干擾。 - 限流鍵設計
限流鍵格式為rate_limit:用戶標識:接口路徑
,例如rate_limit:test_token:/api/order/submit
,精確控制「用戶 + 接口」的訪問頻率。 - 分布式鎖的作用
由于 Redis 的INCR
操作雖然原子,但在高并發下可能出現「讀取 - 判斷 - 更新」的競態條件,因此通過 Redisson 分布式鎖確保計數邏輯的原子性。 - 自動過期機制
每個限流鍵都設置了與時間窗口相同的過期時間,避免 Redis 中存儲大量無效數據,同時確保時間窗口結束后自動重置計數。
由于 Redis 的INCR
操作雖然原子,但在高并發下可能出現「讀取 - 判斷 - 更新」的競態條件,因此通過 Redisson 分布式鎖確保計數邏輯的原子性。 - 自動過期機制
每個限流鍵都設置了與時間窗口相同的過期時間,避免 Redis 中存儲大量無效數據,同時確保時間窗口結束后自動重置計數。