Redis 多規(guī)則限流和防重復(fù)提交方案實(shí)現(xiàn)小結(jié)
Redis 如何實(shí)現(xiàn)限流的,但是大部分都有一個(gè)缺點(diǎn),就是只能實(shí)現(xiàn)單一的限流,比如 1 分鐘訪問 1 次或者 60 分鐘訪問 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)訪問次數(shù)。
一:使用 String 結(jié)構(gòu)記錄固定時(shí)間段內(nèi)某用戶 IP 訪問某接口的次數(shù)
- RedisKey = prefix : className : methodName
- RedisVlue = 訪問次數(shù)
攔截請(qǐng)求:
- 初次訪問時(shí)設(shè)置 [RedisKey] [RedisValue=1] [規(guī)定的過(guò)期時(shí)間];
- 獲取 RedisValue 是否超過(guò)規(guī)定次數(shù),超過(guò)則攔截,未超過(guò)則對(duì) RedisKey 進(jìn)行加1。
規(guī)則是每分鐘訪問 1000 次
- 假設(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)行訪問
二:使用 Zset 進(jìn)行存儲(chǔ),解決臨界值訪問問題
三:實(shí)現(xiàn)多規(guī)則限流
①、先確定最終需要的效果(能實(shí)現(xiàn)多種限流規(guī)則+能實(shí)現(xiàn)防重復(fù)提交)
@RateLimiter( rules = { // 60秒內(nèi)只能訪問10次 @RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS), // 120秒內(nèi)只能訪問20次 @RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS) }, // 防重復(fù)提交 (5秒鐘只能訪問1次) preventDuplicate = true )
②、注解編寫
RateLimiter 注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RateLimiter { /** * 限流key */ String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX; /** * 限流類型 ( 默認(rèn) Ip 模式 ) */ LimitTypeEnum limitType() default LimitTypeEnum.IP; /** * 錯(cuò)誤提示 */ ResultCode message() default ResultCode.REQUEST_MORE_ERROR; /** * 限流規(guī)則 (規(guī)則不可變,可多規(guī)則) */ RateRule[] rules() default {}; /** * 防重復(fù)提交值 */ boolean preventDuplicate() default false; /** * 防重復(fù)提交默認(rèn)值 */ RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5); }
RateRule 注解:
@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RateRule { /** * 限流次數(shù) */ long count() default 10; /** * 限流時(shí)間 */ long time() default 60; /** * 限流時(shí)間單位 */ TimeUnit timeUnit() default TimeUnit.SECONDS; }
③、攔截注解 RateLimiter
- 確定 Redis 存儲(chǔ)方式
RedisKey = prefix : className : methodName
RedisScore = 時(shí)間戳
RedisValue = 任意分布式不重復(fù)的值即可 - 編寫生成 RedisKey 的方法
public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) { StringBuffer key = new StringBuffer(rateLimiter.key()); // 不同限流類型使用不同的前綴 switch (rateLimiter.limitType()) { // XXX 可以新增通過(guò)參數(shù)指定參數(shù)進(jìn)行限流 case IP: key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":"); break; case USER_ID: SysUserDetails user = SecurityUtil.getUser(); if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":"); break; case GLOBAL: break; } MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); key.append(targetClass.getSimpleName()).append("-").append(method.getName()); return key.toString(); }
④、編寫Lua腳本(兩種將事件添加到Redis的方法)
Ⅰ: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ù) local key = KEYS[1] local uuid = KEYS[2] local currentTime = tonumber(KEYS[3]) -- 2. 以數(shù)組最大值為 ttl 最大值 local expireTime = -1; -- 3. 遍歷數(shù)組查看是否超過(guò)限流規(guī)則 for i = 1, #ARGV, 2 do local rateRuleCount = tonumber(ARGV[i]) local rateRuleTime = tonumber(ARGV[i + 1]) -- 3.1 判斷在單位時(shí)間內(nèi)訪問次數(shù) local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime) -- 3.2 判斷是否超過(guò)規(guī)定次數(shù) if tonumber(count) >= rateRuleCount then return true end -- 3.3 判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間 if rateRuleTime > expireTime then 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) return false
Ⅱ、根據(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í)間值問題
以下為第二種實(shí)現(xiàn)方式,在并發(fā)高的情況下效率低,value 是通過(guò)時(shí)間戳進(jìn)行添加,但是訪問量大的話會(huì)使得一直在調(diào)用 redis.call(‘ZADD’, key, currentTime, currentTime),但是在不沖突 value 的情況下,會(huì)比生成 UUID 好。
-- 1. 獲取參數(shù) local key = KEYS[1] local currentTime = KEYS[2] -- 2. 以數(shù)組最大值為 ttl 最大值 local expireTime = -1; -- 3. 遍歷數(shù)組查看是否越界 for i = 1, #ARGV, 2 do local rateRuleCount = tonumber(ARGV[i]) local rateRuleTime = tonumber(ARGV[i + 1]) -- 3.1 判斷在單位時(shí)間內(nèi)訪問次數(shù) local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime) -- 3.2 判斷是否超過(guò)規(guī)定次數(shù) if tonumber(count) >= rateRuleCount then return true end -- 3.3 判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間 if rateRuleTime > expireTime then 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 漏記的問題 ) -- 6.1 maxRetries 最大重試次數(shù) retries 重試次數(shù) local maxRetries = 5 local retries = 0 while true do local result = redis.call('ZADD', key, currentTime, currentTime) if result == 1 then -- 6.2 添加成功則跳出循環(huán) break else -- 6.3 未添加成功則 value + 1 再次進(jìn)行嘗試 retries = retries + 1 if retries >= maxRetries then -- 6.4 超過(guò)最大嘗試次數(shù) 采用添加隨機(jī)數(shù)策略 local random_value = math.random(1, 1000) currentTime = currentTime + random_value else currentTime = currentTime + 1 end end end return false
⑤、編寫AOP攔截
@Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedisScript<Boolean> limitScript; /** * 限流 * XXX 對(duì)限流要求比較高,可以使用在 Redis中對(duì)規(guī)則進(jìn)行存儲(chǔ)校驗(yàn) 或者使用中間件 * * @param joinPoint joinPoint * @param rateLimiter 限流注解 */ @Before(value = "@annotation(rateLimiter)") public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) { // 1. 生成 key String key = getCombineKey(rateLimiter, joinPoint); try { // 2. 執(zhí)行腳本返回是否限流 Boolean flag = 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); throw new ServiceException(rateLimiter.message()); } } catch (ServiceException e) { throw e; } catch (Exception e) { e.printStackTrace(); } } /** * 獲取規(guī)則 * * @param rateLimiter 獲取其中規(guī)則信息 * @return */ private Long[] getRules(RateLimiter rateLimiter) { int capacity = 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; }
到此這篇關(guān)于Redis 多規(guī)則限流和防重復(fù)提交方案實(shí)現(xiàn)小結(jié)的文章就介紹到這了,更多相關(guān)Redis 多規(guī)則限流和防重復(fù)提交內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)
測(cè)試過(guò)程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測(cè)試過(guò)程中經(jīng)常有人會(huì)清空redis,回歸測(cè)試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下2024-03-03詳解Redis命令和鍵_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Redis命令用于在redis服務(wù)器上執(zhí)行某些操作,下面通過(guò)本文給大家分享Redis命令和鍵,需要的的朋友參考下吧2017-08-08Redis隊(duì)列和阻塞隊(duì)列的實(shí)現(xiàn)
本文主要介紹了Redis隊(duì)列和阻塞隊(duì)列的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11NestJS+Redis實(shí)現(xiàn)緩存步驟詳解
這篇文章主要介紹了NestJS+Redis實(shí)現(xiàn)緩存,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08在Ubuntu?14.04系統(tǒng)上備份和恢復(fù)Redis數(shù)據(jù)詳細(xì)步驟
這篇文章主要給大家介紹了關(guān)于在Ubuntu?14.04系統(tǒng)上備份和恢復(fù)Redis數(shù)據(jù)的詳細(xì)步驟,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Redis具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-04-04Redis遍歷所有key的兩個(gè)命令(KEYS 和 SCAN)
這篇文章主要介紹了Redis遍歷所有key的兩個(gè)命令(KEYS 和 SCAN),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04