基于Redis實(shí)現(xiàn)API接口訪問(wèn)次數(shù)限制
一,概述
日常開(kāi)發(fā)中會(huì)有一個(gè)常見(jiàn)的需求,需要限制接口在單位時(shí)間內(nèi)的訪問(wèn)次數(shù),比如說(shuō)某個(gè)免費(fèi)的接口限制單個(gè)IP一分鐘內(nèi)只能訪問(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)只允許訪問(wèn)1000次的限流方案,如下圖01:00s-02:00s之間只允許訪問(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.*; /** * 接口訪問(wèn)頻率注解,默認(rèn)一分鐘只能訪問(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)注解的限制訪問(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è)置客戶端訪問(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); // 查詢窗口內(nèi)已經(jī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í)附上上面中引用到自定義工具類
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)只允許訪問(wèn)3次,超過(guò)就拋出異常
(5)訪問(wèn)測(cè)試
前3次訪問(wèn),接口正常訪問(wèn)
后面的訪問(wèn),返回自定義異常的結(jié)果
到此這篇關(guān)于基于Redis實(shí)現(xiàn)API接口訪問(wèn)次數(shù)限制的文章就介紹到這了,更多相關(guān)Redis API接口訪問(wèn)限制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis數(shù)據(jù)一致性的實(shí)現(xiàn)示例
所謂的redis數(shù)據(jù)一致性即當(dāng)進(jìn)行修改或者保存、刪除之后,redis中的數(shù)據(jù)也應(yīng)該進(jìn)行相應(yīng)變化,本文主要介紹了redis數(shù)據(jù)一致性,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄項(xiàng)目示例(附源碼)
手機(jī)登錄驗(yàn)證在很多網(wǎng)頁(yè)上都得到使用,本文主要介紹了基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄項(xiàng)目示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Redis模擬延時(shí)隊(duì)列實(shí)現(xiàn)日程提醒的方法
文章介紹了如何使用Redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的延時(shí)任務(wù)隊(duì)列,通過(guò)Redis的有序集合特性來(lái)存儲(chǔ)和管理延時(shí)任務(wù),通過(guò)定期檢查集合中小于等于當(dāng)前時(shí)間的任務(wù)并執(zhí)行,可以實(shí)現(xiàn)延時(shí)任務(wù)的管理,感興趣的朋友跟隨小編一起看看吧2024-11-11