SpringBoot + Redis 實現(xiàn)API接口限流的幾種方法
了解Redis
Redis(Remote Dictionary Server)是一個開源的高性能鍵值對存儲數(shù)據(jù)庫。它支持多種數(shù)據(jù)結(jié)構(gòu),包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。Redis的特點(diǎn)包括:
- 內(nèi)存存儲:Redis將數(shù)據(jù)存儲在內(nèi)存中,因此讀寫速度非???,適用于對性能有較高要求的場景。
- 持久化:Redis支持持久化將內(nèi)存中的數(shù)據(jù)保存到硬盤上,以便在服務(wù)器重啟后能夠恢復(fù)數(shù)據(jù)。
- 數(shù)據(jù)結(jié)構(gòu)多樣:Redis不僅僅支持簡單的鍵值對存儲,還支持豐富的數(shù)據(jù)結(jié)構(gòu),例如列表、集合、有序集合等,使其具備更多的功能和用途。
- 高并發(fā):Redis是單線程模型,通過使用異步I/O和非阻塞I/O來支持高并發(fā)。
- 多語言支持:Redis支持多種編程語言的客戶端,如Java、Python、C#等,便于開發(fā)人員在不同平臺上使用。
- 發(fā)布/訂閱:Redis支持發(fā)布/訂閱模式,允許客戶端訂閱一個或多個頻道并接收對應(yīng)頻道的消息。
- 事務(wù)支持:Redis支持事務(wù),可以在一個事務(wù)中執(zhí)行多個命令,并保證這些命令的原子性。
由于Redis具有高性能、靈活的數(shù)據(jù)結(jié)構(gòu)和豐富的功能,它被廣泛用于緩存、消息隊列、計數(shù)器、實時排行榜、會話管理等多種應(yīng)用場景。
需求&為什么需要接口限流
需求:針對相同IP,60s的接口請求次數(shù)不能超過10000次
接口限流是為了保護(hù)系統(tǒng)和服務(wù),防止因為過多的請求而導(dǎo)致系統(tǒng)過載、性能下降甚至崩潰。以下是進(jìn)行接口限流的幾個主要原因:
- 防止惡意攻擊:接口限流可以防止惡意用戶或者攻擊者通過大量的請求來攻擊系統(tǒng),保護(hù)系統(tǒng)的穩(wěn)定性和安全性。
- 保護(hù)系統(tǒng)資源:對于一些計算密集型或者資源消耗較大的接口,限制請求的頻率可以避免服務(wù)器資源被過度消耗,保障其他正常請求的處理。
- 避免雪崩效應(yīng):當(dāng)某個服務(wù)不可用或者響應(yīng)時間過長時,如果沒有限流措施,大量請求可能會涌入后端,導(dǎo)致更多的請求失敗,產(chǎn)生雪崩效應(yīng)。
- 提升系統(tǒng)性能:限流可以控制并發(fā)請求數(shù),避免過多的請求導(dǎo)致服務(wù)器負(fù)載過高,從而提升系統(tǒng)的整體性能和響應(yīng)速度。
- 提供公平資源分配:通過限流,可以實現(xiàn)對不同用戶或者不同服務(wù)請求的公平分配,避免某些請求占用過多資源而影響其他請求。
綜上所述,進(jìn)行接口限流是保護(hù)系統(tǒng)和提升性能的重要手段,對于高并發(fā)的系統(tǒng)尤為重要。通過合理設(shè)置限流策略,可以有效地平衡資源利用和系統(tǒng)穩(wěn)定性,提供更好的用戶體驗。
實現(xiàn)方案
方案一:固定時間段
思路:
當(dāng)用戶在第一次訪問該接口時,向Redis中設(shè)置一個包含了用戶IP和接口方法名的key,value的值初始化為1(表示第一次訪問當(dāng)前接口),同時設(shè)置該key的過期時間(60秒),只要此Redis的key沒有過期,每次訪問都將value的值自增1次,用戶每次訪問接口前,先從Redis中拿到當(dāng)前接口訪問次數(shù),如果發(fā)現(xiàn)訪問次數(shù)大于規(guī)定的次數(shù)(超過10000次),則向用戶返回接口訪問失敗的標(biāo)識。
實現(xiàn):
(一)攔截器
1、添加Redis依賴:首先在pom.xml文件中添加Spring Data Redis依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、 配置Redis連接信息:在application.properties或application.yml中配置Redis的連接信息,包括主機(jī)、端口、密碼等。
3、創(chuàng)建限流攔截器:在項目中創(chuàng)建一個限流攔截器,用于對用戶IP進(jìn)行接口限流。攔截器可以實現(xiàn)HandlerInterceptor接口,并重寫preHandle方法進(jìn)行限流邏輯。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
public class RateLimitInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ipAddress = getIpAddress(request);
String uri = request.getRequestURI().replace("/","_");
String key = "apiVisits:" + uri + ":" + ipAddress;
// 判斷是否已經(jīng)達(dá)到限流次數(shù)
String value = redisTemplate.opsForValue().get(key);
// key 不存在,則是第一次請求設(shè)置過期時間
if(StringUtils.isBlank(value)){
redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, time, TimeUnit.SECONDS);
return true;
}
if (value != null && Integer.parseInt(value) > 10) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
return false;
}
// 未達(dá)到限流次數(shù),自增
redisTemplate.opsForValue().increment(key, 1);
return true;
}
private String getIpAddress(HttpServletRequest request) {
// 從請求頭或代理頭中獲取真實IP地址
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
}
4、注冊攔截器:在配置類中注冊自定義的限流攔截器。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RateLimitInterceptor rateLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
}
}
(二)AOP
以注解+切面的方式實現(xiàn),將需要進(jìn)行限流的API加上注解即可
1、創(chuàng)建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
/**
* 緩存key
*/
String key() default "apiVisits:";
/**
* 限流時間,單位秒
*/
int time() default 5;
/**
* 限流次數(shù)
*/
int count() default 10;
}2、創(chuàng)建AOP切面
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {
private final RedisTemplate redisTemplate;
/**
* 帶有注解的方法之前執(zhí)行
*/
@SuppressWarnings("unchecked")
@Before("@annotation(currentLimiting)")
public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
int time = currentLimiting.time();
int count = currentLimiting.count();
// 將接口方法和用戶IP構(gòu)建Redis的key
String key = getCurrentLimitingKey(currentLimiting.key(), point);
// 判斷是否已經(jīng)達(dá)到限流次數(shù)
String value = redisTemplate.opsForValue().get(key);
if (value != null && Integer.parseInt(value) > count) {
log.error("接口限流,key:{},count:{},currentCount:{}", key, count, value);
throw new RuntimeException("訪問過于頻繁,請稍后再試!");
}
// 未達(dá)到限流次數(shù),自增
redisTemplate.opsForValue().increment(key, 1);
// key 不存在,則是第一次請求設(shè)置過期時間
if(StringUtils.isBlank(value)){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
}
/**
* 組裝 redis 的 key
*/
private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
StringBuilder sb = new StringBuilder(prefixKey);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
sb.append( Utils.getIpAddress(request) );
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
return sb.append("_").append( targetClass.getName() )
.append("_").append(method.getName()).toString();
}
}缺陷:
當(dāng)在10:00訪問接口,這個時候向Reids寫入一條數(shù)據(jù)訪問次數(shù)為1,在10:59的時候突然訪問了9999次,然后redis過期,在11:00訪問了9999次,這樣出現(xiàn)的問題就是在10:59到11:00之間訪問了9999+9999次。故以固定時間段的方式進(jìn)行限流可能會不起作用,會存在Reids過期的臨界點(diǎn)內(nèi)造成大量的用戶訪問。
方案二:滑動窗口
思路:
由于方案一的時間是固定的,我們可以把固定的時間段改成動態(tài)的,也就是在用戶每次訪問接口時,記錄當(dāng)前用戶訪問的時間點(diǎn)(時間戳),并計算前一分鐘內(nèi)用戶訪問該接口的總次數(shù)。如果總次數(shù)大于限流次數(shù),則不允許用戶訪問該接口。這樣就能保證在任意時刻用戶的訪問次數(shù)不會超過10000次。
實現(xiàn):
1、創(chuàng)建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
/**
* 緩存key
*/
String key() default "apiVisits:";
/**
* 限流時間,單位秒
*/
int time() default 5;
/**
* 限流次數(shù)
*/
int count() default 10;
}2、創(chuàng)建AOP切面
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {
private final RedisTemplate redisTemplate;
/**
* 帶有注解的方法之前執(zhí)行
*/
@SuppressWarnings("unchecked")
@Before("@annotation(currentLimiting)")
public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
int time = currentLimiting.time();
int count = currentLimiting.count();
// 將接口方法和用戶IP構(gòu)建Redis的key
String key = getCurrentLimitingKey(currentLimiting.key(), point);
// 使用Zset的 score 設(shè)置成用戶訪問接口的時間戳
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
// 當(dāng)前時間戳
long currentTime = System.currentTimeMillis();
zSetOperations.add(key, currentTime, currentTime);
// 設(shè)置過期時間防止key不消失
redisTemplate.expire(key, time, TimeUnit.SECONDS);
// 移除 time 秒之前的訪問記錄,動態(tài)時間段
zSetOperations.removeRangeByScore(key, 0, currentTime - time * 1000);
// 獲得當(dāng)前時間窗口內(nèi)的訪問記錄數(shù)
Long currentCount = zSetOperations.zCard(key);
// 限流判斷
if (currentCount > count) {
log.error("接口限流,key:{},count:{},currentCount:{}", key, count, currentCount);
throw new RuntimeException("訪問過于頻繁,請稍后再試!");
}
}
/**
* 組裝 redis 的 key
*/
private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
StringBuilder sb = new StringBuilder(prefixKey);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
sb.append( Utils.getIpAddress(request) );
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
return sb.append("_").append( targetClass.getName() )
.append("_").append(method.getName()).toString();
}
}到此這篇關(guān)于SpringBoot + Redis 實現(xiàn)API接口限流的幾種方法的文章就介紹到這了,更多相關(guān)SpringBoot Redis API接口限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決java.lang.ClassNotFoundException: com.mysql.cj.jdbc.D
這篇文章主要介紹了解決java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver報錯問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
jackson在springboot中的使用方式-自定義參數(shù)轉(zhuǎn)換器
這篇文章主要介紹了jackson在springboot中的使用方式-自定義參數(shù)轉(zhuǎn)換器,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
Spring Cloud Feign內(nèi)部實現(xiàn)代碼細(xì)節(jié)
Feign 的英文表意為“假裝,偽裝,變形”, 是一個http請求調(diào)用的輕量級框架,可以以Java接口注解的方式調(diào)用Http請求,而不用像Java中通過封裝HTTP請求報文的方式直接調(diào)用。接下來通過本文給大家分享Spring Cloud Feign內(nèi)部實現(xiàn)代碼細(xì)節(jié),感興趣的朋友一起看看吧2021-05-05
Spring?Boot?3中一套可以直接用于生產(chǎn)環(huán)境的Log4J2日志配置詳解
Log4J2是Apache Log4j的升級版,參考了logback的一些優(yōu)秀的設(shè)計,并且修復(fù)了一些問題,因此帶來了一些重大的提升,這篇文章主要介紹了Spring?Boot?3中一套可以直接用于生產(chǎn)環(huán)境的Log4J2日志配置,需要的朋友可以參考下2023-12-12

