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

