基于Redis實(shí)現(xiàn)API接口訪(fǎng)問(wèn)次數(shù)限制
一,概述
日常開(kāi)發(fā)中會(huì)有一個(gè)常見(jiàn)的需求,需要限制接口在單位時(shí)間內(nèi)的訪(fǎng)問(wèn)次數(shù),比如說(shuō)某個(gè)免費(fèi)的接口限制單個(gè)IP一分鐘內(nèi)只能訪(fǎng)問(wèn)5次。該怎么實(shí)現(xiàn)呢,通常大家都會(huì)想到用redis,確實(shí)通過(guò)redis可以實(shí)現(xiàn)這個(gè)功能,下面實(shí)現(xiàn)一下。
二,常見(jiàn)錯(cuò)誤
固定時(shí)間窗口
有人設(shè)計(jì)了一個(gè)在每分鐘內(nèi)只允許訪(fǎng)問(wèn)1000次的限流方案,如下圖01:00s-02:00s之間只允許訪(fǎng)問(wèn)1000次。這種設(shè)計(jì)的問(wèn)題在于,請(qǐng)求可能在01:59s-02:00s之間被請(qǐng)求1000次,02:00s-02:01s之間被請(qǐng)求了1000次,這種情況下01:59s-02:01s間隔0.02s之間被請(qǐng)求2000次,很顯然這種設(shè)計(jì)是錯(cuò)誤的。

三, 實(shí)現(xiàn)
1,基于滑動(dòng)時(shí)間窗口

在指定的時(shí)間窗口內(nèi)次數(shù)是累積的,超過(guò)閾值,都會(huì)限制。
2,流程如下

3,代碼實(shí)現(xiàn)
前提:pom文件引入redis,Spring AOP等
(1)添加注解RequestLimit
package com.xxx.demo.aspect;
import java.lang.annotation.*;
/**
* 接口訪(fǎng)問(wèn)頻率注解,默認(rèn)一分鐘只能訪(fǎng)問(wèn)10次
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
// 限制時(shí)間 單位:秒(默認(rèn)值:一分鐘)
long period() default 60;
// 允許請(qǐng)求的次數(shù)(默認(rèn)值:10次)
long count() default 10;
}(2)添加切面實(shí)現(xiàn)注解的限制訪(fǎng)問(wèn)邏輯
package com.xxx.demo.aspect;
import com.xgd.demo.commons.ErrorCode;
import com.xgd.demo.handler.BusinessException;
import com.xgd.demo.util.IpUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;
/**
* @date 2024/11/8 上午8:43
*/
@Aspect
@Component
@Log4j2
public class RequestLimitAspect {
@Autowired
RedisTemplate redisTemplate;
@Pointcut("@annotation(requestLimit)")
public void controllerAspect(RequestLimit requestLimit) {}
@Around("controllerAspect(requestLimit)")
public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
// 從注解中獲取限制次數(shù)和窗口時(shí)間
long period = requestLimit.period();
long limitCount = requestLimit.count();
// 請(qǐng)求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String ip = IpUtil.getIpFromRequest(request);
String uri = request.getRequestURI();
//設(shè)置客戶(hù)端訪(fǎng)問(wèn)的key
String key = "req_limit_".concat(uri).concat(ip);
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
// 添加當(dāng)前時(shí)間戳,分?jǐn)?shù)為當(dāng)前時(shí)間戳
long currentMs = System.currentTimeMillis();
zSetOperations.add(key, currentMs, currentMs);
// 設(shè)置窗口時(shí)間作為過(guò)期時(shí)間
redisTemplate.expire(key, period, TimeUnit.SECONDS);
// 移除掉不在窗口里的數(shù)據(jù)
zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000);
// 查詢(xún)窗口內(nèi)已經(jīng)訪(fǎng)問(wèn)過(guò)的次數(shù)
Long count = zSetOperations.zCard(key);
if (count > limitCount) {
log.error("接口攔截:{} 請(qǐng)求超過(guò)限制頻率【{}次/{}s】,IP為{}", uri, limitCount, period, ip);
throw new BusinessException(ErrorCode.REQUEST_LIMITED.getCode(), ErrorCode.REQUEST_LIMITED.getMessage());
}
// 繼續(xù)執(zhí)行請(qǐng)求
return joinPoint.proceed();
}
}上面里面請(qǐng)求被攔截,是拋出了一個(gè)自定義的業(yè)務(wù)異常,大家可以根據(jù)自己的情況自己定義。
(3)同時(shí)附上上面中引用到自定義工具類(lèi)
package com.xxx.demo.util;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* @date 2024/11/8 上午9:06
*/
public class IpUtil {
private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";
private static final String X_REAL_IP_HEADER = "X-Real-IP";
/**
* 從請(qǐng)求中獲取IP
*
* @return IP;當(dāng)獲取不到時(shí),返回null
*/
public static String getIpFromRequest(HttpServletRequest request ) {
return getRealIp(request);
}
/**
* 獲取請(qǐng)求的真實(shí)IP,優(yōu)先級(jí)從高到低為:<br/>
* 1.從請(qǐng)求頭X-Forwarded-For中獲取ip,并且只獲取第一個(gè)ip(從左到右) <br/>
* 2.從請(qǐng)求頭X-Real-IP中獲取ip <br/>
* 3.使用{@link HttpServletRequest#getRemoteAddr()}方法獲取ip
*
* @param request 請(qǐng)求對(duì)象,必須不能為null
* @return ip
*/
private static String getRealIp(HttpServletRequest request) {
Objects.requireNonNull(request, "request must be not null");
String ip = request.getHeader(X_FORWARDED_FOR_HEADER);
if (ip != null && !ip.isBlank()) {
int delimiterIndex = ip.indexOf(',');
if (delimiterIndex != -1) {
// 如果存在多個(gè)ip,則取第一個(gè)ip
ip = ip.substring(0, delimiterIndex);
}
return ip;
}
ip = request.getHeader(X_REAL_IP_HEADER);
if (ip != null && !ip.isBlank()) {
return ip;
} else {
return request.getRemoteAddr();
}
}
}(4)使用注解
這里限制為10秒內(nèi)只允許訪(fǎng)問(wèn)3次,超過(guò)就拋出異常

(5)訪(fǎng)問(wèn)測(cè)試
前3次訪(fǎng)問(wèn),接口正常訪(fǎng)問(wèn)

后面的訪(fǎng)問(wèn),返回自定義異常的結(jié)果

到此這篇關(guān)于基于Redis實(shí)現(xiàn)API接口訪(fǎng)問(wèn)次數(shù)限制的文章就介紹到這了,更多相關(guān)Redis API接口訪(fǎng)問(wèn)限制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis哈希Hash鍵值對(duì)集合操作(查詢(xún)?cè)黾有薷?
Redis中的Hash數(shù)據(jù)?是一個(gè)?鍵值對(duì)集合,本文主要介紹了Redis哈希Hash鍵值對(duì)集合操作(查詢(xún)?cè)黾有薷?,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
redis數(shù)據(jù)結(jié)構(gòu)之壓縮列表
這篇文章主要介紹了redis數(shù)據(jù)結(jié)構(gòu)之壓縮列表,壓縮列表是列表list和hash數(shù)據(jù)結(jié)構(gòu)的底層實(shí)現(xiàn)之一,是redis為了節(jié)約內(nèi)存而開(kāi)發(fā)的,由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu),下面詳細(xì)內(nèi)容需要的小伙伴可以參考一下2022-03-03
Redis?HyperLogLog數(shù)據(jù)量統(tǒng)計(jì)的實(shí)現(xiàn)實(shí)例
在大數(shù)據(jù)時(shí)代,統(tǒng)計(jì)海量數(shù)據(jù)中的唯一值是一個(gè)常見(jiàn)的需求,但同時(shí)也是極具挑戰(zhàn)性的任務(wù),傳統(tǒng)的統(tǒng)計(jì)方法可能會(huì)消耗大量?jī)?nèi)存或計(jì)算資源,而?Redis?的?HyperLogLog?數(shù)據(jù)結(jié)構(gòu)?則提供了一種高效、輕量的解決方案,下面就來(lái)詳細(xì)介紹一下HyperLogLog的使用,感興趣的可以了解一下2025-09-09
在ssm項(xiàng)目中使用redis緩存查詢(xún)數(shù)據(jù)的方法
本文主要簡(jiǎn)單的使用Java代碼進(jìn)行redis緩存,即在查詢(xún)的時(shí)候先在service層從redis緩存中獲取數(shù)據(jù)。如果大家對(duì)在ssm項(xiàng)目中使用redis緩存查詢(xún)數(shù)據(jù)的相關(guān)知識(shí)感興趣的朋友跟隨腳本之家小編一起看看吧2018-03-03
mac下設(shè)置redis開(kāi)機(jī)啟動(dòng)方法步驟
這篇文章主要介紹了mac下設(shè)置redis開(kāi)機(jī)啟動(dòng),本文詳細(xì)的給出了操作步驟,需要的朋友可以參考下2015-07-07

