springboot自定義注解RateLimiter限流注解技術(shù)文檔詳解
什么是限流
限流是一種控制系統(tǒng)訪問(wèn)頻率的技術(shù)手段,就像高速公路的收費(fèi)站控制車流量一樣。
生活場(chǎng)景類比:
- 銀行ATM機(jī):每張卡每天最多取款5次
- 手機(jī)驗(yàn)證碼:每個(gè)手機(jī)號(hào)每分鐘最多發(fā)送1條
- 網(wǎng)站登錄:每個(gè)IP每分鐘最多嘗試5次
技術(shù)價(jià)值:
- 防止惡意攻擊:阻止暴力破解、惡意爬蟲
- 保護(hù)系統(tǒng)穩(wěn)定:避免瞬間大量請(qǐng)求壓垮服務(wù)器
- 提升用戶體驗(yàn):確保正常用戶的訪問(wèn)質(zhì)量
- 節(jié)約成本:減少不必要的資源消耗
系統(tǒng)架構(gòu)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用戶請(qǐng)求 │───→│ 限流切面 │───→│ 業(yè)務(wù)接口 │
│ (HTTP API) │ │ (AOP攔截) │ │ (Controller) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 限流服務(wù) │
│ (核心邏輯處理) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 緩存存儲(chǔ) │
│ (EhCache/Redis) │
└─────────────────┘工作流程:
- 用戶發(fā)起HTTP請(qǐng)求
- Spring AOP切面攔截帶有@RateLimiter注解的方法
- 限流服務(wù)根據(jù)注解配置生成限流鍵
- 從緩存中獲取當(dāng)前訪問(wèn)次數(shù)
- 判斷是否超過(guò)限制,決定放行或拒絕
- 更新緩存中的計(jì)數(shù)器
核心組件詳解
1. 限流注解 (@RateLimiter)
這是系統(tǒng)的核心注解,定義了限流的各種參數(shù):
package cn.jbolt.config.anno.rateLimiter;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RateLimiter {
/**
* 緩存前綴 - 用于區(qū)分不同業(yè)務(wù)的限流數(shù)據(jù)
*/
String prefix() default "jblimit:";
/**
* 時(shí)間窗口(秒) - 限流的時(shí)間范圍
*/
int time() default 60;
/**
* 允許訪問(wèn)次數(shù) - 時(shí)間窗口內(nèi)最大訪問(wèn)次數(shù)
*/
@AliasFor(attribute = "count")
int value() default 12;
/**
* 限制類型 - 決定按什么維度限流
*/
RateLimitType limitType() default RateLimitType.DEFAULT;
/**
* 限制提示消息 - 觸發(fā)限流時(shí)返回的錯(cuò)誤信息
*/
String msg() default "操作過(guò)于頻繁,請(qǐng)稍后重試";
/**
* 允許訪問(wèn)次數(shù) - 與value互為別名
*/
@AliasFor(attribute = "value")
int count() default 12;
/**
* 自定義鍵 - 當(dāng)limitType為CUSTOM時(shí)使用
*/
String customKey() default "";
/**
* 是否啟用 - 可用于動(dòng)態(tài)開關(guān)限流功能
*/
boolean enabled() default true;
/**
* 額外的時(shí)間窗口限制(秒)
* 實(shí)現(xiàn)雙重限流:比如1秒最多1次 + 1分鐘最多10次
*/
int extraTime() default -1;
/**
* 額外時(shí)間窗口內(nèi)的允許訪問(wèn)次數(shù)
*/
int extraCount() default -1;
/**
* 額外限制的提示消息
*/
String extraMsg() default "";
}2. 限流類型枚舉 (RateLimitType)
package cn.jbolt.config.anno.rateLimiter;
public enum RateLimitType {
/**
* 默認(rèn)限制(全局)
* 所有請(qǐng)求共享一個(gè)計(jì)數(shù)器
*/
DEFAULT,
/**
* 基于IP地址限制
* 每個(gè)IP獨(dú)立計(jì)數(shù)
*/
IP,
/**
* 基于用戶ID限制
* 每個(gè)登錄用戶獨(dú)立計(jì)數(shù)
*/
USER,
/**
* 基于自定義KEY限制
* 根據(jù)業(yè)務(wù)邏輯自定義限流維度
*/
CUSTOM
}3. 限流異常類 (RateLimitException)
package cn.jbolt.config.exception;
public class RateLimitException extends RuntimeException {
private final String message;
private final int retryAfter;
public RateLimitException(String message) {
this(message, 0);
}
public RateLimitException(String message, int retryAfter) {
super(message);
this.message = message;
this.retryAfter = retryAfter;
}
@Override
public String getMessage() {
return message;
}
public int getRetryAfter() {
return retryAfter;
}
}4. 全局異常處理器 (RateLimitExceptionHandler)
package cn.jbolt.config.handler;
import cn.jbolt.config.exception.RateLimitException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class RateLimitExceptionHandler {
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<Map<String, Object>> handleRateLimitException(
RateLimitException e, HttpServletResponse response) {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
result.put("message", e.getMessage());
result.put("data", null);
// 設(shè)置HTTP響應(yīng)頭,告訴客戶端多久后可以重試
if (e.getRetryAfter() > 0) {
response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
}
response.setHeader("X-RateLimit-Window", "60");
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
}
}5. IP工具類 (IpUtils)
package cn.jbolt.util;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
private static final String[] IP_HEADER_NAMES = {
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IPV4 = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
/**
* 獲取客戶端真實(shí)IP地址
* 處理代理服務(wù)器、負(fù)載均衡器等場(chǎng)景
*/
public static String getClientIp(HttpServletRequest request) {
if (request == null) {
return UNKNOWN;
}
String ip = null;
// 依次檢查各種可能的IP頭
for (String header : IP_HEADER_NAMES) {
ip = request.getHeader(header);
if (isValidIp(ip)) {
break;
}
}
// 如果頭信息中沒(méi)有找到,則使用getRemoteAddr
if (!isValidIp(ip)) {
ip = request.getRemoteAddr();
if (LOCALHOST_IPV6.equals(ip)) {
ip = LOCALHOST_IPV4;
}
}
// 處理多個(gè)IP的情況(X-Forwarded-For可能包含多個(gè)IP)
if (StringUtils.hasText(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return StringUtils.hasText(ip) ? ip : UNKNOWN;
}
/**
* 檢查IP是否有效
*/
private static boolean isValidIp(String ip) {
return StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip);
}
}技術(shù)實(shí)現(xiàn)原理
1. AOP切面攔截
系統(tǒng)使用Spring AOP在方法執(zhí)行前進(jìn)行攔截,這是一個(gè)核心的限流切面類:
package cn.jbolt.config.aspect;
import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import cn.jbolt.config.exception.RateLimitException;
import cn.jbolt.util.IpUtils;
import cn.jbolt.util.cache.RateLimiterCache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class RateLimiterAspect {
private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);
@Around("@annotation(rateLimiter)")
public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
// 檢查是否啟用限流
if (!rateLimiter.enabled()) {
return point.proceed();
}
// 獲取HTTP請(qǐng)求對(duì)象
HttpServletRequest request = getCurrentRequest();
if (request == null) {
logger.warn("無(wú)法獲取HttpServletRequest,跳過(guò)限流檢查");
return point.proceed();
}
// 生成限流鍵
String limitKey = generateLimitKey(point, rateLimiter, request);
// 執(zhí)行主要限流檢查
checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg());
// 執(zhí)行額外限流檢查(如果配置了)
if (rateLimiter.extraTime() > 0 && rateLimiter.extraCount() > 0) {
String extraLimitKey = limitKey + ":extra";
String extraMsg = rateLimiter.extraMsg().isEmpty() ? rateLimiter.msg() : rateLimiter.extraMsg();
checkRateLimit(extraLimitKey, rateLimiter.extraTime(), rateLimiter.extraCount(), extraMsg);
}
// 所有限流檢查通過(guò),繼續(xù)執(zhí)行業(yè)務(wù)方法
return point.proceed();
}
/**
* 執(zhí)行限流檢查
*/
private void checkRateLimit(String key, int timeWindow, int maxCount, String message) {
try {
// 增加計(jì)數(shù)器并獲取當(dāng)前訪問(wèn)次數(shù)
int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS);
logger.debug("限流檢查: key={}, 當(dāng)前次數(shù)={}, 限制次數(shù)={}", key, currentCount, maxCount);
// 檢查是否超過(guò)限制
if (currentCount > maxCount) {
long ttl = RateLimiterCache.getTtl(key);
logger.warn("觸發(fā)限流: key={}, 當(dāng)前次數(shù)={}, 限制次數(shù)={}, 剩余時(shí)間={}秒",
key, currentCount, maxCount, ttl);
throw new RateLimitException(message, (int) ttl);
}
} catch (RateLimitException e) {
throw e;
} catch (Exception e) {
logger.error("限流檢查異常: key={}", key, e);
// 限流服務(wù)異常時(shí),選擇放行而不是阻塞
}
}
/**
* 生成限流鍵
*/
private String generateLimitKey(ProceedingJoinPoint point, RateLimiter rateLimiter, HttpServletRequest request) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(rateLimiter.prefix());
// 添加方法簽名
String methodSignature = point.getSignature().toShortString();
keyBuilder.append(methodSignature);
// 根據(jù)限流類型添加不同的標(biāo)識(shí)
switch (rateLimiter.limitType()) {
case IP:
keyBuilder.append(":ip:").append(IpUtils.getClientIp(request));
break;
case USER:
String userId = getCurrentUserId(request);
keyBuilder.append(":user:").append(userId != null ? userId : "anonymous");
break;
case CUSTOM:
keyBuilder.append(":custom:").append(rateLimiter.customKey());
break;
case DEFAULT:
default:
keyBuilder.append(":default:global");
break;
}
// 添加時(shí)間窗口,確保不同時(shí)間窗口的限流獨(dú)立
keyBuilder.append(":").append(rateLimiter.time());
String finalKey = keyBuilder.toString();
logger.debug("生成限流鍵: {}", finalKey);
return finalKey;
}
/**
* 獲取當(dāng)前HTTP請(qǐng)求
*/
private HttpServletRequest getCurrentRequest() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attrs != null ? attrs.getRequest() : null;
} catch (Exception e) {
logger.warn("獲取HttpServletRequest失敗", e);
return null;
}
}
/**
* 獲取當(dāng)前用戶ID
* 這里需要根據(jù)實(shí)際的用戶認(rèn)證體系來(lái)實(shí)現(xiàn)
*/
private String getCurrentUserId(HttpServletRequest request) {
// 方案1:從Session中獲取
Object userId = request.getSession().getAttribute("userId");
if (userId != null) {
return userId.toString();
}
// 方案2:從JWT Token中獲取
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
// 解析JWT獲取用戶ID
// return JwtUtils.getUserIdFromToken(token);
}
// 方案3:從請(qǐng)求參數(shù)中獲取
String userIdParam = request.getParameter("userId");
if (userIdParam != null) {
return userIdParam;
}
return null;
}
}2. 緩存數(shù)據(jù)結(jié)構(gòu)
系統(tǒng)使用一個(gè)包裝類來(lái)存儲(chǔ)緩存數(shù)據(jù):
package cn.jbolt.util.cache;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
public class CacheWrapper implements Serializable {
private static final long serialVersionUID = 1L;
private Object value;
private long timestamp;
private long durationMillis;
public CacheWrapper() {
}
public CacheWrapper(Object value, long duration, TimeUnit unit) {
this.value = value;
this.timestamp = System.currentTimeMillis();
this.durationMillis = unit.toMillis(duration);
}
/**
* 檢查是否已過(guò)期
*/
public boolean isExpired() {
return System.currentTimeMillis() - timestamp > durationMillis;
}
/**
* 獲取剩余過(guò)期時(shí)間(毫秒)
*/
public long getRemainingTime() {
long elapsed = System.currentTimeMillis() - timestamp;
return Math.max(0, durationMillis - elapsed);
}
// getter和setter方法
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getDurationMillis() {
return durationMillis;
}
public void setDurationMillis(long durationMillis) {
this.durationMillis = durationMillis;
}
}完整代碼示例
1. 控制器示例
package cn.jbolt.controller;
import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class DemoController {
/**
* 登錄接口 - 防止暴力破解
* 每個(gè)IP每分鐘最多嘗試5次
*/
@PostMapping("/login")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 5,
msg = "登錄嘗試過(guò)于頻繁,請(qǐng)1分鐘后重試"
)
public Result login(@RequestBody LoginRequest request) {
// 登錄邏輯
if (isValidUser(request.getUsername(), request.getPassword())) {
return Result.success("登錄成功");
} else {
return Result.error("用戶名或密碼錯(cuò)誤");
}
}
/**
* 發(fā)送驗(yàn)證碼 - 防止惡意發(fā)送
* 每個(gè)IP每分鐘最多3次
*/
@PostMapping("/sms/send")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 3,
msg = "驗(yàn)證碼發(fā)送過(guò)于頻繁,請(qǐng)稍后重試"
)
public Result sendSms(@RequestBody SmsRequest request) {
// 發(fā)送短信邏輯
boolean success = smsService.sendCode(request.getPhone());
return success ? Result.success("發(fā)送成功") : Result.error("發(fā)送失敗");
}
/**
* 查詢接口 - 防止爬蟲
* 每個(gè)IP每分鐘最多100次
*/
@GetMapping("/products")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 100,
msg = "查詢過(guò)于頻繁,請(qǐng)稍后重試"
)
public Result getProducts(@RequestParam(defaultValue = "1") int page) {
// 查詢商品邏輯
List<Product> products = productService.getProducts(page);
return Result.success(products);
}
/**
* 用戶操作 - 防止頻繁操作
* 每個(gè)用戶每分鐘最多30次
*/
@PostMapping("/user/update")
@RateLimiter(
limitType = RateLimitType.USER,
time = 60,
count = 30,
msg = "操作過(guò)于頻繁,請(qǐng)稍后重試"
)
public Result updateUser(@RequestBody UserUpdateRequest request) {
// 更新用戶信息邏輯
boolean success = userService.updateUser(request);
return success ? Result.success("更新成功") : Result.error("更新失敗");
}
/**
* 關(guān)鍵操作 - 嚴(yán)格限流
* 1秒最多1次 + 1分鐘最多5次
*/
@PostMapping("/transfer")
@RateLimiter(
limitType = RateLimitType.USER,
time = 1, count = 1, msg = "操作過(guò)于頻繁,請(qǐng)稍后再試",
extraTime = 60, extraCount = 5, extraMsg = "您在1分鐘內(nèi)的操作次數(shù)已達(dá)上限"
)
public Result transfer(@RequestBody TransferRequest request) {
// 轉(zhuǎn)賬邏輯
boolean success = transferService.transfer(request);
return success ? Result.success("轉(zhuǎn)賬成功") : Result.error("轉(zhuǎn)賬失敗");
}
/**
* 自定義限流 - 按商品限制
* 每個(gè)商品每分鐘最多下單20次
*/
@PostMapping("/order/{productId}")
@RateLimiter(
limitType = RateLimitType.CUSTOM,
customKey = "product_order",
time = 60,
count = 20,
msg = "該商品下單過(guò)于頻繁,請(qǐng)稍后重試"
)
public Result createOrder(@PathVariable String productId, @RequestBody OrderRequest request) {
// 創(chuàng)建訂單邏輯
Order order = orderService.createOrder(productId, request);
return Result.success(order);
}
// 輔助方法
private boolean isValidUser(String username, String password) {
// 實(shí)際的用戶驗(yàn)證邏輯
return "admin".equals(username) && "123456".equals(password);
}
}2. 統(tǒng)一返回對(duì)象
package cn.jbolt.common;
public class Result {
private int code;
private String message;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static Result error(String message) {
Result result = new Result();
result.code = 500;
result.message = message;
result.data = null;
return result;
}
// getter和setter方法
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}使用指南
1. 基本使用
// 最簡(jiǎn)單的用法 - 使用默認(rèn)配置
@RateLimiter(limitType = RateLimitType.IP)
public String simpleApi() {
return "success";
}
// 自定義時(shí)間窗口和次數(shù)
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, // 60秒
count = 100 // 最多100次
)
public String customApi() {
return "success";
}2. 不同場(chǎng)景的配置建議
// 登錄接口 - 嚴(yán)格限制
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 5,
msg = "登錄嘗試過(guò)于頻繁,請(qǐng)1分鐘后重試"
)
// 查詢接口 - 適中限制
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 100,
msg = "查詢過(guò)于頻繁,請(qǐng)稍后重試"
)
// 用戶操作 - 按用戶限制
@RateLimiter(
limitType = RateLimitType.USER,
time = 60, count = 30,
msg = "操作過(guò)于頻繁,請(qǐng)稍后重試"
)
// 全局保護(hù) - 系統(tǒng)級(jí)限制
@RateLimiter(
limitType = RateLimitType.DEFAULT,
time = 60, count = 200,
msg = "系統(tǒng)繁忙,請(qǐng)稍后重試"
)3. 雙重限流配置
// 嚴(yán)格的雙重限流:秒級(jí) + 分鐘級(jí)
@RateLimiter(
limitType = RateLimitType.IP,
time = 1, count = 1, msg = "請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試",
extraTime = 60, extraCount = 10, extraMsg = "您在1分鐘內(nèi)的請(qǐng)求次數(shù)已達(dá)上限"
)
// 適中的雙重限流:分鐘級(jí) + 小時(shí)級(jí)
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 100, msg = "1分鐘內(nèi)請(qǐng)求過(guò)多",
extraTime = 3600, extraCount = 1000, extraMsg = "1小時(shí)內(nèi)請(qǐng)求過(guò)多"
)總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot參數(shù)驗(yàn)證的幾種方式小結(jié)
在日常的接口開發(fā)中,為了防止非法參數(shù)對(duì)業(yè)務(wù)造成影響,經(jīng)常需要對(duì)接口的參數(shù)進(jìn)行校驗(yàn),例如登錄的時(shí)候需要校驗(yàn)用戶名和密碼是否為空,所以本文介紹了SpringBoot參數(shù)驗(yàn)證的幾種方式,需要的朋友可以參考下2024-07-07
Mybatis配置之properties和settings標(biāo)簽的用法
這篇文章主要介紹了Mybatis配置之properties和settings標(biāo)簽的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java Map 在put值時(shí)value值不被覆蓋的解決辦法
這篇文章主要介紹了Java Map 在put值時(shí)value值不被覆蓋的解決辦法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-04-04
詳解Spring Cloud中Hystrix 線程隔離導(dǎo)致ThreadLocal數(shù)據(jù)丟失
這篇文章主要介紹了詳解Spring Cloud中Hystrix 線程隔離導(dǎo)致ThreadLocal數(shù)據(jù)丟失,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03

