詳解Redisson分布式限流的使用及原理
1. 常見的分布式限流算法
1.1 固定窗口算法
原理:固定窗口算法是一種簡單的限流算法。在固定時間窗口內(nèi),記錄請求的數(shù)量。例如,設定每10秒鐘最多允許5次請求,如果在當前窗口內(nèi)的請求數(shù)超過限制,則拒絕請求。
優(yōu)點:簡單易實現(xiàn)。
缺點:存在“流量突刺”的問題,也就是在窗口切換時可能會產(chǎn)生兩倍于閾值流量的請求
適用場景: 適合于對于突發(fā)流量容忍度高的場景。
1.2 滑動窗口算法
原理: 滑動窗口算法在固定窗口的基礎上,將一個計時窗口分成了若干個小窗口,然后每個小窗口維護一個獨立的計數(shù)器。當請求的時間大于當前窗口的最大時間時,則將計時窗口向前平移一個小窗口。平移時,將第一個小窗口的數(shù)據(jù)丟棄,然后將第二個小窗口設置為第一個小窗口,同時在最后面新增一個小窗口,將新的請求放在新增的小窗口中。同時要保證整個窗口中所有小窗口的請求數(shù)目之后不能超過設定的閾值。
優(yōu)點: 相比滑動窗口,它能夠更精確地控制請求頻率,避免了“流量突刺”問題。
缺點: 滑動窗口算法是固定窗口的一種改進,但從根本上并沒有真正解決固定窗口算法的臨界突發(fā)流量問題。實現(xiàn)相對復雜,要求記錄每個時間窗口的請求數(shù),需要更多的存儲和計算資源。
適用場景: 比較適合對于不能容忍臨界突發(fā)流量的場景。
1.3 漏桶算法
原理: 漏桶算法將請求放入一個桶中,桶以固定速率漏出請求。如果桶滿了,新的請求將被丟棄。請求的流入速率可以不均勻,但漏出的速率是固定的,確保請求按照固定速率被處理。
優(yōu)點: 適用于需要平滑處理突發(fā)流量的場景,能夠把瞬時流量平滑到平穩(wěn)流量。
缺點: 請求的流入速率過快會導致請求丟失,需要合理設置桶的容量和漏水速率。不支持突發(fā)流量。
適用場景: 例如保護數(shù)據(jù)庫的限流,先把對數(shù)據(jù)庫的訪問加入到桶中,工作線程再以數(shù)據(jù)庫能夠承受的請求壓力從桶中取出請求,去訪問數(shù)據(jù)庫。
1.4 令牌桶算法
原理:令牌桶算法是對漏斗算法的一種改進,除了能夠起到限流的作用外,還允許一定程度的流量突發(fā)。在令牌桶算法中,存在一個令牌桶,算法中存在一種機制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果滿了令牌就無法放進去了。當請求來時,會首先到令牌桶中去拿令牌,如果拿到了令牌,則該請求會被處理,并消耗掉拿到的令牌;如果令牌桶為空,則該請求會被丟棄。
優(yōu)點: 允許突發(fā)流量,且能平滑處理請求流量。
缺點:對桶的大小和令牌生成速率要求較高,對存儲資源的需求較大,因為需要維護令牌桶。
適用場景: 適合電商搶購或者微博出現(xiàn)熱點事件這種場景,因為在限流的同時可以應對一定的突發(fā)流量。如果采用漏桶那樣的均勻速度處理請求的算法,在發(fā)生熱點時間的時候,會造成大量的用戶無法訪問,對用戶體驗的損害比較大。
2. Redisson分布式限流的使用
Redisson 是一款基于 Redis 的 Java 分布式工具庫。它封裝了 Redis 的底層操作,提供了分布式鎖、分布式集合、分布式隊列、分布式執(zhí)行器等功能,幫助開發(fā)者更高效地實現(xiàn)分布式系統(tǒng)。Redisson 提供了一系列高級的數(shù)據(jù)結(jié)構(gòu)接口(如 RMap, RSet, RLock 等),并支持多種部署模式(單節(jié)點、主從、哨兵、集群等)。
在生產(chǎn)環(huán)境下,我們使用的更多的是其分布式鎖功能,但是Redisson還提供了分布式限流的功能,相比于 Guava 提供的單機限流、Sentinel提供的接口限流,Redisson的分布式限流更便于用戶去自定義符合自身業(yè)務的限流規(guī)則。
2.1 引入Maven依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.43.0</version>
</dependency>
2.2 封裝Redisson限流工具類
我這里針對Redisson做了二次封裝,配合配置文件實現(xiàn)更靈活的限流策略
限流配置類RateLimitProperties:
import lombok.Data;
import org.redisson.api.RateIntervalUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties("rate-limit")
@Data
public class RateLimitProperties {
private Config defaultConfig;
private List<Config> configs;
@Data
public static class Config {
private String keyPrefix;
private long timeOut;
private RateIntervalUnit rateIntervalUnit;
private long count;
}
/**
* 根據(jù)key獲取對應匹配的前綴的限流配置
* @param key key
* @return 限流配置
*/
public Config getRateLimitConfig(String key) {
return configs.stream()
.filter(e -> key.startsWith(e.getKeyPrefix()))
.findFirst()
.orElse(defaultConfig);
}
}
限流配置文件application.yaml:
# 限流配置
rate-limit:
# 默認限流配置,如果前綴沒有匹配上,則使用默認配置
default-config:
model-name-prefix: default
time-out: 30
rate-interval-unit: seconds
count: 1
configs:
- key-prefix: test
time-out: 1
rate-interval-unit: seconds
count: 2
限流工具類RateLimitUtil:
import com.gzb.app.config.RateLimitProperties;
import com.gzb.app.constant.RedisConstant;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 限流工具類
*/
@Component
public class RateLimitUtil {
@Autowired
private RateLimitProperties rateLimitProperties;
@Autowired
private RedissonClient redissonClient;
/**
* 嘗試獲取一個限流器許可,如果當前沒有足夠的令牌,調(diào)用線程將被阻塞,直到有足夠的令牌
* @param key 限流器的唯一標識
*/
public void acquire(String key) {
getRedissonRateLimiter(key).acquire();
}
/**
* 嘗試立即獲取一個限流器許可,如果當前沒有足夠的令牌,立即返回 false
* @param key 限流器的唯一標識
* @return 成功:true 失?。篺alse
*/
public boolean tryAcquire(String key) {
return getRedissonRateLimiter(key).tryAcquire();
}
/**
* 嘗試在指定時間內(nèi)獲取一個限流器許可,如果在指定時間內(nèi)沒有足夠的令牌,立即返回 false
* @param key 限流器的唯一標識
* @param time 等待的時間長度
* @param timeUnit 時間單位
* @return 成功:true 失敗:false
*/
public boolean tryAcquire(String key, long time, TimeUnit timeUnit) {
return getRedissonRateLimiter(key).tryAcquire(time, timeUnit);
}
/**
* 獲取限流器,如果限流器不存在,則根據(jù)配置創(chuàng)建一個新的限流器
* @param key 限流器的唯一標識
* @return RedissonRateLimiter 限流器實例
*/
public RRateLimiter getRedissonRateLimiter(String key) {
RateLimitProperties.Config config = rateLimitProperties.getRateLimitConfig(key);
long count = config.getCount();
long timeOut = config.getTimeOut();
RateIntervalUnit rateIntervalUnit = config.getRateIntervalUnit();
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 如果限流器不存在,就創(chuàng)建一個限流器
if (!rateLimiter.isExists()) {
rateLimiter.trySetRate(RateType.OVERALL, count, timeOut, rateIntervalUnit);
return rateLimiter;
}
checkIfConfigNeedUpdate(rateLimiter, config);
return rateLimiter;
}
/**
* 檢查限流器配置是否發(fā)生修改,如果配置發(fā)生變化,則刪除舊的限流器并重新創(chuàng)建
*
* @param rateLimiter 限流器
* @param rateLimitConfig 限流配置
*/
private void checkIfConfigNeedUpdate(RRateLimiter rateLimiter, RateLimitProperties.Config rateLimitConfig) {
long count = rateLimitConfig.getCount();
long timeOut = rateLimitConfig.getTimeOut();
RateIntervalUnit rateIntervalUnit = rateLimitConfig.getRateIntervalUnit();
// 獲取之前限流器的配置信息
RateLimiterConfig config = rateLimiter.getConfig();
if (rateIntervalUnit.toMillis(timeOut) != config.getRateInterval() || count != config.getRate()) {
// 如果當前限流器的配置與配置文件不一致,說明服務器重啟過
rateLimiter.delete();
// 配置文件為準,重新設置
rateLimiter.trySetRate(RateType.OVERALL, count, timeOut, rateIntervalUnit);
}
}
}
2.3 測試代碼
@RestController
@Slf4j
public class RedissonController {
@Autowired
private RateLimitUtil rateLimitUtil;
@GetMapping("/rateLimit/{key}")
public void rateLimit(@PathVariable String key) {
log.info("{} start get access token, current time = {}", Thread.currentThread().getName(), System.currentTimeMillis());
rateLimitUtil.acquire(key);
log.info("{} successfully start get access token, current time = {}", Thread.currentThread().getName(), System.currentTimeMillis());
}
}
3. Redisson分布式限流原理
3.1 設置限流配置方法trySetRate
在使用限流器之前,都需要提前設置限流配置,否則限流代碼會報錯(后面會提到具體哪一行代碼)
跟進RedissonRateLimiter的trySetRate方法,可以找到下圖中的方法,可見設置限流配置很簡單,只是一段很簡單的Lua腳本,在腳本中往Hash數(shù)據(jù)結(jié)構(gòu)中寫入了我們之前配置的限流參數(shù)。

3.2 acquire限流方法原理
跟進RedissonRateLimiter的acquire方法,可以找到一段Lua腳本代碼,在展開講解Lua腳本之前,需要先了解一下限流需要用到哪些Redis鍵值

圖片中我使用的key是test:
首先是{key},對應著圖片中的test,是哈希數(shù)據(jù)類型,里面存儲著限流配置,比如:rate、interval等信息,這里的keepAliveTime先不做考慮,與本次要講解的核心限流邏輯無太大關系。

接著是{key}:value,對應著圖片中的{test}:value,是字符串數(shù)據(jù)類型,value是一個數(shù)字,表示可用的令牌桶數(shù)量。

再接著是{key}:permits,對應著圖片中的{test}:permits,是一個有序集合,保存著限流過程中的訪問信息。有序集合中的value使用隨機數(shù)和你要獲取的token數(shù)量拼接作為value,防止value的重復,score存儲著訪問時候的時間戳。

-- 獲取限流器的配置值:rate、interval 和 type
local rate = redis.call('hget', KEYS[1], 'rate'); -- 獲取限制速率(每秒允許的請求次數(shù))
local interval = redis.call('hget', KEYS[1], 'interval'); -- 獲取限流的時間間隔(單位通常為秒)
local type = redis.call('hget', KEYS[1], 'type'); -- 獲取限流類型(如 OVERALL 或 PRE_WARMING)
-- 如果配置沒有初始化(即獲取的值為空),拋出錯誤
-- 這里對應著一開始要設置好限流配置參數(shù),否則Lua腳本執(zhí)行過程會拋出異常
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')
-- 當前請求令牌數(shù)的鍵
local valueName = KEYS[2];
-- 請求令牌記錄的鍵
local permitsName = KEYS[4];
-- 如果限流類型是 PRE_WARMING,調(diào)整鍵值名
if type == '1' then
valueName = KEYS[3]; -- 如果是 PRE_WARMING,則使用不同的令牌數(shù)鍵
permitsName = KEYS[5]; -- 如果是 PRE_WARMING,則使用不同的令牌記錄鍵
end;
-- 檢查請求的令牌數(shù)是否超過了令牌桶的大小
-- 例如:rate設置為10,在acqure的時候大于10,Lua腳本就會報錯
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount cannot exceed defined rate');
-- 獲取當前剩余可用的令牌數(shù)
local currentValue = redis.call('get', valueName);
-- 變量 res 用于存儲返回值,指示是否可以獲取令牌
-- 如果是nil表示令牌充足,獲取令牌成功
-- 如果是一個數(shù)字,則表示令牌不足,需要要等待的時間
local res;
-- 表明不是第一次獲取令牌
if currentValue ~= false then
-- 獲取在過去一段時間內(nèi)過期的請求記錄
local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);
local released = 0; -- 已釋放的令牌數(shù)
-- 統(tǒng)計過期的請求記錄中的令牌數(shù)
for i, v in ipairs(expiredValues) do
local random, permits = struct.unpack('Bc0I', v);
released = released + permits;
end;
-- 如果有過期的令牌,進行釋放處理
if released > 0 then
-- 從令牌記錄中移除過期的項
redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);
-- 以下if-else代碼用于保證可用的令牌個數(shù)和令牌訪問記錄中使用的令牌個數(shù)之和小于桶的容量rate
if tonumber(currentValue) + released > tonumber(rate) then
local values = redis.call('zrange', permitsName, 0, -1); -- 獲取所有令牌記錄
local used = 0; -- 已使用的令牌數(shù)
-- 統(tǒng)計所有已使用的令牌數(shù)
for i, v in ipairs(values) do
local random, permits = struct.unpack('Bc0I', v);
used = used + permits;
end;
-- 調(diào)整當前令牌數(shù),確保不會超過桶的大小
currentValue = tonumber(rate) - used;
else
-- 否則,直接增加釋放的令牌數(shù)
currentValue = tonumber(currentValue) + released;
end;
-- 更新令牌數(shù)
redis.call('set', valueName, currentValue);
end;
-- 如果當前令牌數(shù)小于請求的令牌數(shù),計算下一次可以獲取令牌的時間
if tonumber(currentValue) < tonumber(ARGV[1]) then
local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores'); -- 獲取最早的請求記錄
res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2])); -- 計算等待時間
else
-- 如果當前令牌數(shù)足夠,記錄請求并減少令牌數(shù)
redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));
redis.call('decrby', valueName, ARGV[1]);
res = nil; -- 沒有錯誤,返回值為空
end;
-- 如果是第一次獲取令牌,則設置當前可用的令牌數(shù),添加訪問記錄,扣減可用令牌數(shù)量
else
-- 設置當前可用的令牌數(shù)
redis.call('set', valueName, rate);
-- 新增領票訪問記錄
redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));
-- 扣減可用令牌數(shù)量
redis.call('decrby', valueName, ARGV[1]);
-- 沒有錯誤,返回值為空,表示獲取令牌成功
res = nil;
end;
-- 返回結(jié)果,可能是等待時間或空值
-- 如果是nil表示令牌充足,獲取令牌成功
-- 如果是一個數(shù)字,則表示令牌不足,需要要等待的時間
return res;
這里有一張流程圖可以很好的演示Lua腳本的限流邏輯:

3.3 使用Redisson分布式限流的注意事項
限流配置Rate不要設置過大
從Lua腳本中我們可以看到,Rate的值限制著有序集合中的限流訪問記錄,如果Rate值過大可能會導致有序集合中存儲的數(shù)據(jù)激增,從而Redis的內(nèi)存增加,并且使得Lua腳本的執(zhí)行效率下降,緊跟著就是限流功能的效率下降,傾向于小Rate+小時間窗口的方式,這種設置方式請求也會更均勻一些。
限流的上限取決于Redis的性能
RRateLimiter的限流能力受制于Redis實例的性能上限。例如,如果Redis實例的QPS上限是1w,那么通過RRateLimiter實現(xiàn)2w QPS限流是不可能的。要突破單個Redis實例性能的限制,可以通過拆分多個限流器來實現(xiàn)。具體做法是創(chuàng)建多個限流器,使用不同的名稱,并在各臺機器上隨機選擇一個限流器進行限流,這樣總流量就可以被分散到多個限流器上,從而提升整體限流上限。
到此這篇關于詳解Redisson分布式限流的使用及原理的文章就介紹到這了,更多相關Redisson分布式限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺析SpringBoot中使用thymeleaf找不到.HTML文件的原因
這篇文章主要介紹了SpringBoot中使用thymeleaf找不到.HTML文件的原因分析,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
換了最新的idea如何將原來舊版本的idea設置導進新的idea中
這篇文章主要介紹了換了最新的idea如何將原來舊版本的idea設置導進新的idea中,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11
你知道怎么用Spring的三級緩存解決循環(huán)依賴嗎
這篇文章主要為大家詳細介紹了Spring的三級緩存解決循環(huán)依賴,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02
利用HttpUrlConnection 上傳 接收文件的實現(xiàn)方法
下面小編就為大家?guī)硪黄肏ttpUrlConnection 上傳 接收文件的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11
java線程并發(fā)控制同步工具CountDownLatch
這篇文章主要為大家介紹了java線程并發(fā)控制同步工具CountDownLatch使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版)
這篇文章主要介紹了SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-10-10

