SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流的示例
使用redis的自增對接口進(jìn)行限流
1.引入依賴
<!-- springboot已集成,不需要再引入版本 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.代碼示例
2.1 基本代碼
我這里使用使用了手機(jī)號和一些其他的字符串組成了redis的key,你可以自定義自己的key.
private void validRateBasic (String phone) { String key = "LIMIT:RATE:" + phone; redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); try { String num = (String) redisTemplate.opsForValue().get(key); if (ObjectUtil.isNull(num)) { redisTemplate.opsForValue().set(key, "1", 60, TimeUnit.SECONDS); } else if (Integer.parseInt(num) >= 20) { Long expire = redisTemplate.getExpire(key); throw new CheckedException("操作頻繁,請" + expire + "s后再試"); } else { redisTemplate.opsForValue().increment(key); } } catch (Exception e) { if (e instanceof CheckedException) { throw new CheckedException(e.getMessage()); } else { log.info("校驗(yàn)上傳速率失敗,error:{}",e); throw new CheckedException("操作失敗,請稍后再試"); } } }
這段代碼實(shí)現(xiàn)了同一個(gè)接口中,同一個(gè)手機(jī)號在60s內(nèi)只能訪問20次,雖然redis是單線程的,但在高并發(fā)情況下,這段代碼仍有并發(fā)問題。 在獲取訪問次數(shù)和增加訪問次數(shù)之間,訪問次數(shù)可能已經(jīng)被其他線程修改 。如果你對多出來的一兩次請求要求不高,那這個(gè)限制基本符合需求。
在redis中,我們可以使用lua腳本和redis事務(wù)來保證操作的原子性。
2.2 使用redis事務(wù)
2.2.1 SessionCallback(不推薦)
有人使用redisTemplate.setEnableTransactionSupport(true),使用redisTemplate支持事務(wù),但這樣可能存在已下幾種問題:
- 如果你在分布式環(huán)境中使用Redis,事務(wù)支持可能會有問題,因?yàn)镽edis的事務(wù)模型是樂觀鎖,如果在事務(wù)中的操作被其他實(shí)例修改,那么事務(wù)就會失敗。在高并發(fā)場景中,這可能會導(dǎo)致大量的事務(wù)失敗。
- 使RedisTemplate支持事務(wù)會導(dǎo)致所有的Redis操作都在事務(wù)中執(zhí)行,這可能會降低性能,特別是在需要執(zhí)行大量Redis操作的情況下。
- 這個(gè)設(shè)置將影響所有使用這個(gè)RedisTemplate實(shí)例的代碼,所以需要確保所有相關(guān)的代碼都能正確地處理在事務(wù)中的Redis操作。
這里使用的是Spring Data Redis提供的會話回調(diào)(SessionCallback)接口。它可以讓我們在一個(gè)Redis連接中執(zhí)行多個(gè)操作,并保持原子性。
private void validRate(String phone) { String key = "LIMIT:RATE:" + phone;SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流 int retryTimes = 0; // 失敗重試五次 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); while(retryTimes < 6) { retryTimes++; try { // 在事務(wù)之外獲取這個(gè)鍵的值 String num = (String) redisTemplate.opsForValue().get(key); // 使用SessionCallback進(jìn)行原子性操作 SessionCallback<Object> sessionCallback = new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.watch(key); operations.multi(); // 在事務(wù)內(nèi)部再次檢查這個(gè)鍵的值 String currentNum = (String) operations.opsForValue().get(key); if (num == null ? currentNum != null : !num.equals(currentNum)) { // 這個(gè)鍵的值被修改了,所以取消這個(gè)事務(wù) operations.discard(); return null; } if (ObjectUtil.isNull(num)) { operations.opsForValue().set(key, "1", 60, TimeUnit.SECONDS); } else if (Integer.parseInt(num) >= 5) { Long expire = operations.getExpire(key); throw new CheckedException("操作頻繁,請" + expire + "s后再試"); } else { operations.opsForValue().increment(key); } // 提交事務(wù)并返回結(jié)果 return operations.exec(); } }; // 執(zhí)行SessionCallback List<Object> results = (List<Object>) redisTemplate.execute(sessionCallback); if (CollectionUtils.isEmpty(results)) { // 如果事務(wù)執(zhí)行失敗,重新嘗試事務(wù) log.info("重試"); continue; } return; } catch (Exception e) { // 在重試的情況下捕獲任何異常 if (retryTimes >= 5) { throw new CheckedException("操作頻繁,請稍后再試"); } } } }
這一段代碼看起來沒啥毛病,一運(yùn)行你會發(fā)現(xiàn) String num = (String) operations.opsForValue().get(key);一直是null。這是因?yàn)樵趓edis事務(wù)中,事務(wù)中的所有命令都會被放在隊(duì)列中,等到exec命令被調(diào)用時(shí)才會一次性執(zhí)行。redis的事務(wù)在某些方面是不如關(guān)系型數(shù)據(jù)庫的:
- 無隔離性:redis的事務(wù)沒有隔離性,在事務(wù)開始(multi命令執(zhí)行)之后,其他的客戶端仍然可以對事務(wù)中的鍵進(jìn)行讀寫操作,這可能會影響到事務(wù)的結(jié)果。
- 無原子讀:無法讀取到自己事務(wù)未提交的數(shù)據(jù),也無法讀取到其他事務(wù)寫入的數(shù)據(jù)。如上面代碼,事務(wù)開始后的get命令返回的是null,而不是最新數(shù)據(jù)。
- 無回滾:一旦一個(gè)事務(wù)被提交(exec命令執(zhí)行),事務(wù)中的所有操作都會被執(zhí)行,即使其中某些操作失敗了,其他的操作也不會被回滾。
- 無鎖:redis事務(wù)并不提供鎖,或者說redis并沒有鎖的概念,和無隔離性造的結(jié)果是一樣的。
2.2.2 分布式鎖(推薦)
分布式鎖已經(jīng)有很多成熟的框架了和很多優(yōu)秀的博客了,這里就不贅述了,有空會補(bǔ)充一篇。
2.3 使用Lua腳本(推薦)
Lua腳本在執(zhí)行時(shí)是原子性的:當(dāng)腳本正在運(yùn)行的時(shí)候,不會有其他的腳本或Redis命令被執(zhí)行。
private void validRateLua(String phone) { String key = "LIMIT:RATE:" + phone; int retryTimes = 0; // 創(chuàng)建Lua腳本,返回新的計(jì)數(shù)值 String luaScript = "local num = redis.call('GET', KEYS[1]);" + "if num == false then " + " redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]);" + " return ARGV[1];" + "elseif tonumber(num) <= tonumber(ARGV[3]) then " + " local newNum = redis.call('INCR', KEYS[1]);" + " return newNum;" + "else " + " return num;" + "end;"; RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); while(retryTimes < 5) { retryTimes++; try { // 執(zhí)行Lua腳本 String num = (String) redisTemplate.execute(redisScript, Collections.singletonList(key), "1", "60","5"); if (num != null && Integer.parseInt(num) > 5) { Long expire = redisTemplate.getExpire(key); throw new CheckedException("操作頻繁,請" + expire + "s后再試"); } return; } catch (Exception e) { if (e instanceof CheckedException) { throw new CheckedException(e.getMessage()); } else { // 在重試的情況下捕獲任何異常 // 有需要的可以加入指數(shù)退避、最大重試時(shí)間等 if (retryTimes >= 5) { log.error("上傳失敗,error:{}",e); throw new CheckedException("操作頻繁,請稍后再試"); } } } } }
執(zhí)行Lua腳本有幾點(diǎn)需要注意:
- lua腳本會阻塞Redis的所有操作,需要盡量保證Lua腳本的執(zhí)行時(shí)間短,以免影響redis的性能.
- lua腳本一旦被執(zhí)行,它就會被加載到內(nèi)存中,即使沒被執(zhí)行也會持續(xù)保存在內(nèi)存中,這樣設(shè)計(jì)的目的是方便快速執(zhí)行,避免每次執(zhí)行腳本都要重新加載
- lua腳本一般都很小,但是如果你有大量的lua腳本長時(shí)間保存在內(nèi)存中,被頻繁的加載和執(zhí)行,就會占用大量的內(nèi)存。這個(gè)問題可以通過script命令和LUA-EVAL-NOLOAD配置選項(xiàng)來解決。
到此這篇關(guān)于SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流的示例的文章就介紹到這了,更多相關(guān)SpringBoot redis計(jì)數(shù)器限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot+Redis實(shí)現(xiàn)API接口防刷限流的項(xiàng)目實(shí)踐
- SpringBoot整合Redis并且用Redis實(shí)現(xiàn)限流的方法 附Redis解壓包
- 基于SpringBoot+Redis實(shí)現(xiàn)一個(gè)簡單的限流器
- SpringBoot使用Redis對用戶IP進(jìn)行接口限流的項(xiàng)目實(shí)踐
- SpringBoot使用Redis對用戶IP進(jìn)行接口限流的示例詳解
- SpringBoot Redis用注釋實(shí)現(xiàn)接口限流詳解
- 使用SpringBoot?+?Redis?實(shí)現(xiàn)接口限流的方式
- SpringBoot中使用Redis對接口進(jìn)行限流的實(shí)現(xiàn)
- springboot+redis 實(shí)現(xiàn)分布式限流令牌桶的示例代碼
相關(guān)文章
Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法示例
這篇文章主要介紹了Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法,結(jié)合具體實(shí)例形式分析了java的遍歷、遞歸、回溯等算法實(shí)現(xiàn)八皇后問題的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-06-06Redis使用RedisTemplate模板類的常用操作方式
這篇文章主要介紹了Redis使用RedisTemplate模板類的常用操作方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Eclipse 開發(fā)java 出現(xiàn)Failed to create the Java Virtual Machine錯(cuò)誤
這篇文章主要介紹了Eclipse 開發(fā)java 出現(xiàn)Failed to create the Java Virtual Machine錯(cuò)誤解決辦法的相關(guān)資料,需要的朋友可以參考下2017-04-04Java字符編碼簡介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java字符編碼簡介,本文主要包括以下幾個(gè)方面:編碼基本知識,Java,系統(tǒng)軟件,url,工具軟件等,感興趣的朋友一起看看吧2017-08-08Java中數(shù)組的使用與注意事項(xiàng)詳解(推薦)
數(shù)組是一組地址連續(xù)、長度固定的具有相同類型的數(shù)據(jù)的集合,通過數(shù)組下標(biāo)我們可以指定數(shù)字中的每一個(gè)元素,下面這篇文章主要給大家介紹了關(guān)于Java中數(shù)組的使用與注意事項(xiàng)的相關(guān)資料,需要的朋友可以參考下2021-08-08