簡(jiǎn)介
市面上很多介紹redis如何實(shí)現(xiàn)限流的,但是大部分都有一個(gè)缺點(diǎn),就是只能實(shí)現(xiàn)單一的限流,比如1分鐘訪問(wèn)1次或者60分鐘訪問(wèn)10次這種,但是如果想一個(gè)接口兩種規(guī)則都需要滿足呢,我們的項(xiàng)目又是分布式項(xiàng)目,應(yīng)該如何解決,下面就介紹一下redis實(shí)現(xiàn)分布式多規(guī)則限流的方式。
思考
如何一分鐘只能發(fā)送一次驗(yàn)證碼,一小時(shí)只能發(fā)送10次驗(yàn)證碼等等多種規(guī)則的限流
如何防止接口被惡意打擊(短時(shí)間內(nèi)大量請(qǐng)求)
如何限制接口規(guī)定時(shí)間內(nèi)訪問(wèn)次數(shù)
解決方法
記錄某IP訪問(wèn)次數(shù)
使用 String結(jié)構(gòu) 記錄固定時(shí)間段內(nèi)某用戶IP訪問(wèn)某接口的次數(shù)
RedisKey = prefix : className : methodName
RedisVlue = 訪問(wèn)次數(shù)
攔截請(qǐng)求:
初次訪問(wèn)時(shí)設(shè)置 「[RedisKey] [RedisValue=1] [規(guī)定的過(guò)期時(shí)間]」
獲取 RedisValue 是否超過(guò)規(guī)定次數(shù),超過(guò)則攔截,未超過(guò)則對(duì) RedisKey 進(jìn)行加1
分析: 規(guī)則是每分鐘訪問(wèn) 1000 次
考慮并發(fā)問(wèn)題
假設(shè)目前 RedisKey => RedisValue 為 999
目前大量請(qǐng)求進(jìn)行到第一步( 獲取Redis請(qǐng)求次數(shù) ),那么所有線程都獲取到了值為999,進(jìn)行判斷都未超過(guò)限定次數(shù)則不攔截,導(dǎo)致實(shí)際次數(shù)超過(guò) 1000 次
「解決辦法:」
保證方法執(zhí)行原子性(加鎖、lua)
考慮在臨界值進(jìn)行訪問(wèn)
思考下圖
代碼實(shí)現(xiàn): 比較簡(jiǎn)單,
Zset解決臨界值問(wèn)題
使用 Zset 進(jìn)行存儲(chǔ),解決臨界值訪問(wèn)問(wèn)題
網(wǎng)上幾乎都有實(shí)現(xiàn),這里就不過(guò)多介紹
實(shí)現(xiàn)多規(guī)則限流
先確定最終需要的效果
能實(shí)現(xiàn)多種限流規(guī)則
能實(shí)現(xiàn)防重復(fù)提交
通過(guò)以上要求設(shè)計(jì)注解(先想象出最終實(shí)現(xiàn)效果)
@RateLimiter( rules={ //60秒內(nèi)只能訪問(wèn)10次 @RateRule(count=10,time=60,timeUnit=TimeUnit.SECONDS), //120秒內(nèi)只能訪問(wèn)20次 @RateRule(count=20,time=120,timeUnit=TimeUnit.SECONDS) }, //防重復(fù)提交(5秒鐘只能訪問(wèn)1次) preventDuplicate=true )
編寫注解(RateLimiter,RateRule)
編寫 RateLimiter 注解。
/** *@Description:請(qǐng)求接口限制 *@Author:yiFei */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited public@interfaceRateLimiter{ /** *限流key */ Stringkey()defaultRedisKeyConstants.RATE_LIMIT_CACHE_PREFIX; /** *限流類型(默認(rèn)Ip模式) */ LimitTypeEnumlimitType()defaultLimitTypeEnum.IP; /** *錯(cuò)誤提示 */ ResultCodemessage()defaultResultCode.REQUEST_MORE_ERROR; /** *限流規(guī)則(規(guī)則不可變,可多規(guī)則) */ RateRule[]rules()default{}; /** *防重復(fù)提交值 */ booleanpreventDuplicate()defaultfalse; /** *防重復(fù)提交默認(rèn)值 */ RateRulepreventDuplicateRule()default@RateRule(count=1,time=5); }
編寫RateRule注解
@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public@interfaceRateRule{ /** *限流次數(shù) */ longcount()default10; /** *限流時(shí)間 */ longtime()default60; /** *限流時(shí)間單位 */ TimeUnittimeUnit()defaultTimeUnit.SECONDS; }
攔截注解 RateLimiter
確定redis存儲(chǔ)方式
RedisKey = prefix : className : methodName
RedisScore = 時(shí)間戳
RedisValue = 任意分布式不重復(fù)的值即可
編寫生成 RedisKey 的方法
/** *通過(guò)rateLimiter和joinPoint拼接prefix:ip/userId:classSimpleName-methodName * *@paramrateLimiter提供prefix *@paramjoinPoint提供classSimpleName:methodName *@return */ publicStringgetCombineKey(RateLimiterrateLimiter,JoinPointjoinPoint){ StringBufferkey=newStringBuffer(rateLimiter.key()); //不同限流類型使用不同的前綴 switch(rateLimiter.limitType()){ //XXX可以新增通過(guò)參數(shù)指定參數(shù)進(jìn)行限流 caseIP: key.append(IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":"); break; caseUSER_ID: SysUserDetailsuser=SecurityUtil.getUser(); if(!ObjectUtils.isEmpty(user))key.append(user.getUserId()).append(":"); break; caseGLOBAL: break; } MethodSignaturesignature=(MethodSignature)joinPoint.getSignature(); Methodmethod=signature.getMethod(); Class>targetClass=method.getDeclaringClass(); key.append(targetClass.getSimpleName()).append("-").append(method.getName()); returnkey.toString(); }
編寫lua腳本
編寫lua腳本 (兩種將時(shí)間添加到Redis的方法)。
Zset的UUID value值
UUID(可用其他有相同的特性的值)為Zset中的value值
參數(shù)介紹
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 唯一ID
KEYS[3] = 當(dāng)前時(shí)間
ARGV = [次數(shù),單位時(shí)間,次數(shù),單位時(shí)間, 次數(shù), 單位時(shí)間 ...]
由java傳入分布式不重復(fù)的 value 值
--1.獲取參數(shù) localkey=KEYS[1] localuuid=KEYS[2] localcurrentTime=tonumber(KEYS[3]) --2.以數(shù)組最大值為ttl最大值 localexpireTime=-1; --3.遍歷數(shù)組查看是否超過(guò)限流規(guī)則 fori=1,#ARGV,2do localrateRuleCount=tonumber(ARGV[i]) localrateRuleTime=tonumber(ARGV[i+1]) --3.1判斷在單位時(shí)間內(nèi)訪問(wèn)次數(shù) localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime) --3.2判斷是否超過(guò)規(guī)定次數(shù) iftonumber(count)>=rateRuleCountthen returntrue end --3.3判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間 ifrateRuleTime>expireTimethen expireTime=rateRuleTime end end --4.redis中添加當(dāng)前時(shí)間 redis.call('ZADD',key,currentTime,uuid) --5.更新緩存過(guò)期時(shí)間 redis.call('PEXPIRE',key,expireTime) --6.刪除最大時(shí)間限度之前的數(shù)據(jù),防止數(shù)據(jù)過(guò)多 redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime) returnfalse
根據(jù)時(shí)間戳作為Zset中的value值
參數(shù)介紹
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 當(dāng)前時(shí)間
ARGV = [次數(shù),單位時(shí)間,次數(shù),單位時(shí)間, 次數(shù), 單位時(shí)間 ...]
根據(jù)時(shí)間進(jìn)行生成value值,考慮同一毫秒添加相同時(shí)間值問(wèn)題
以下為第二種實(shí)現(xiàn)方式,在并發(fā)高的情況下效率低,value是通過(guò)時(shí)間戳進(jìn)行添加,但是訪問(wèn)量大的話會(huì)使得一直在調(diào)用 redis.call('ZADD', key, currentTime, currentTime),但是在不沖突value的情況下,會(huì)比生成 UUID 好
--1.獲取參數(shù) localkey=KEYS[1] localcurrentTime=KEYS[2] --2.以數(shù)組最大值為ttl最大值 localexpireTime=-1; --3.遍歷數(shù)組查看是否越界 fori=1,#ARGV,2do localrateRuleCount=tonumber(ARGV[i]) localrateRuleTime=tonumber(ARGV[i+1]) --3.1判斷在單位時(shí)間內(nèi)訪問(wèn)次數(shù) localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime) --3.2判斷是否超過(guò)規(guī)定次數(shù) iftonumber(count)>=rateRuleCountthen returntrue end --3.3判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間 ifrateRuleTime>expireTimethen expireTime=rateRuleTime end end --4.更新緩存過(guò)期時(shí)間 redis.call('PEXPIRE',key,expireTime) --5.刪除最大時(shí)間限度之前的數(shù)據(jù),防止數(shù)據(jù)過(guò)多 redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime) --6.redis中添加當(dāng)前時(shí)間(解決多個(gè)線程在同一毫秒添加相同value導(dǎo)致Redis漏記的問(wèn)題) --6.1maxRetries最大重試次數(shù)retries重試次數(shù) localmaxRetries=5 localretries=0 whiletruedo localresult=redis.call('ZADD',key,currentTime,currentTime) ifresult==1then --6.2添加成功則跳出循環(huán) break else --6.3未添加成功則value+1再次進(jìn)行嘗試 retries=retries+1 ifretries>=maxRetriesthen --6.4超過(guò)最大嘗試次數(shù)采用添加隨機(jī)數(shù)策略 localrandom_value=math.random(1,1000) currentTime=currentTime+random_value else currentTime=currentTime+1 end end end returnfalse
編寫 AOP 攔截
@Autowired privateRedisTemplateredisTemplate; @Autowired privateRedisScript limitScript; /** *限流 *XXX對(duì)限流要求比較高,可以使用在Redis中對(duì)規(guī)則進(jìn)行存儲(chǔ)校驗(yàn)或者使用中間件 * *@paramjoinPointjoinPoint *@paramrateLimiter限流注解 */ @Before(value="@annotation(rateLimiter)") publicvoidboBefore(JoinPointjoinPoint,RateLimiterrateLimiter){ //1.生成key Stringkey=getCombineKey(rateLimiter,joinPoint); try{ //2.執(zhí)行腳本返回是否限流 Booleanflag=redisTemplate.execute(limitScript, ListUtil.of(key,String.valueOf(System.currentTimeMillis())), (Object[])getRules(rateLimiter)); //3.判斷是否限流 if(Boolean.TRUE.equals(flag)){ log.error("ip:'{}'攔截到一個(gè)請(qǐng)求RedisKey:'{}'", IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()), key); thrownewServiceException(rateLimiter.message()); } }catch(ServiceExceptione){ throwe; }catch(Exceptione){ e.printStackTrace(); } } /** *獲取規(guī)則 * *@paramrateLimiter獲取其中規(guī)則信息 *@return */ privateLong[]getRules(RateLimiterrateLimiter){ intcapacity=rateLimiter.rules().length<1; ????//?1.?構(gòu)建?args ????Long[]?args?=?new?Long[rateLimiter.preventDuplicate()???capacity?+?2?:?capacity]; ????//?3.?記錄數(shù)組元素 ????int?index?=?0; ????//?2.?判斷是否需要添加防重復(fù)提交到redis進(jìn)行校驗(yàn) ????if?(rateLimiter.preventDuplicate())?{ ????????RateRule?preventRateRule?=?rateLimiter.preventDuplicateRule(); ????????args[index++]?=?preventRateRule.count(); ????????args[index++]?=?preventRateRule.timeUnit().toMillis(preventRateRule.time()); ????} ????RateRule[]?rules?=?rateLimiter.rules(); ????for?(RateRule?rule?:?rules)?{ ????????args[index++]?=?rule.count(); ????????args[index++]?=?rule.timeUnit().toMillis(rule.time()); ????} ????return?args; }
審核編輯:劉清
-
lua腳本
+關(guān)注
關(guān)注
0文章
21瀏覽量
7613 -
Redis
+關(guān)注
關(guān)注
0文章
379瀏覽量
10966
原文標(biāo)題:Redis 多規(guī)則限流和防重復(fù)提交方案實(shí)現(xiàn)
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
redis分布式鎖場(chǎng)景實(shí)現(xiàn)
在 Java 中利用 redis 實(shí)現(xiàn)一個(gè)分布式鎖服務(wù)
Redis 分布式鎖的正確實(shí)現(xiàn)方式
手?jǐn)]了個(gè)Redis分布式鎖
Redis實(shí)現(xiàn)限流的三種方式分享
如何使用注解實(shí)現(xiàn)redis分布式鎖!
![如何使用注解<b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>redis</b><b class='flag-5'>分布式</b>鎖!](https://file1.elecfans.com/web2/M00/82/3E/wKgZomRHWs-AOyYjAAA4JFzfesc324.png)
分布式限流簡(jiǎn)介
![<b class='flag-5'>分布式</b><b class='flag-5'>限流</b>簡(jiǎn)介](https://file.elecfans.com/web2/M00/A6/33/pYYBAGRjQR6AD2p1AABFXyVVjms987.png)
評(píng)論