詳解Redisson分布式限流的使用及原理
1. 常見的分布式限流算法
1.1 固定窗口算法
原理:固定窗口算法是一種簡單的限流算法。在固定時間窗口內(nèi),記錄請求的數(shù)量。例如,設(shè)定每10秒鐘最多允許5次請求,如果在當(dāng)前窗口內(nèi)的請求數(shù)超過限制,則拒絕請求。
優(yōu)點:簡單易實現(xiàn)。
缺點:存在“流量突刺”的問題,也就是在窗口切換時可能會產(chǎn)生兩倍于閾值流量的請求
適用場景: 適合于對于突發(fā)流量容忍度高的場景。
1.2 滑動窗口算法
原理: 滑動窗口算法在固定窗口的基礎(chǔ)上,將一個計時窗口分成了若干個小窗口,然后每個小窗口維護(hù)一個獨立的計數(shù)器。當(dāng)請求的時間大于當(dāng)前窗口的最大時間時,則將計時窗口向前平移一個小窗口。平移時,將第一個小窗口的數(shù)據(jù)丟棄,然后將第二個小窗口設(shè)置為第一個小窗口,同時在最后面新增一個小窗口,將新的請求放在新增的小窗口中。同時要保證整個窗口中所有小窗口的請求數(shù)目之后不能超過設(shè)定的閾值。
優(yōu)點: 相比滑動窗口,它能夠更精確地控制請求頻率,避免了“流量突刺”問題。
缺點: 滑動窗口算法是固定窗口的一種改進(jìn),但從根本上并沒有真正解決固定窗口算法的臨界突發(fā)流量問題。實現(xiàn)相對復(fù)雜,要求記錄每個時間窗口的請求數(shù),需要更多的存儲和計算資源。
適用場景: 比較適合對于不能容忍臨界突發(fā)流量的場景。
1.3 漏桶算法
原理: 漏桶算法將請求放入一個桶中,桶以固定速率漏出請求。如果桶滿了,新的請求將被丟棄。請求的流入速率可以不均勻,但漏出的速率是固定的,確保請求按照固定速率被處理。
優(yōu)點: 適用于需要平滑處理突發(fā)流量的場景,能夠把瞬時流量平滑到平穩(wěn)流量。
缺點: 請求的流入速率過快會導(dǎo)致請求丟失,需要合理設(shè)置桶的容量和漏水速率。不支持突發(fā)流量。
適用場景: 例如保護(hù)數(shù)據(jù)庫的限流,先把對數(shù)據(jù)庫的訪問加入到桶中,工作線程再以數(shù)據(jù)庫能夠承受的請求壓力從桶中取出請求,去訪問數(shù)據(jù)庫。
1.4 令牌桶算法
原理:令牌桶算法是對漏斗算法的一種改進(jìn),除了能夠起到限流的作用外,還允許一定程度的流量突發(fā)。在令牌桶算法中,存在一個令牌桶,算法中存在一種機(jī)制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果滿了令牌就無法放進(jìn)去了。當(dāng)請求來時,會首先到令牌桶中去拿令牌,如果拿到了令牌,則該請求會被處理,并消耗掉拿到的令牌;如果令牌桶為空,則該請求會被丟棄。
優(yōu)點: 允許突發(fā)流量,且能平滑處理請求流量。
缺點:對桶的大小和令牌生成速率要求較高,對存儲資源的需求較大,因為需要維護(hù)令牌桶。
適用場景: 適合電商搶購或者微博出現(xiàn)熱點事件這種場景,因為在限流的同時可以應(yīng)對一定的突發(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 提供的單機(jī)限流、Sentinel提供的接口限流,Redisson的分布式限流更便于用戶去自定義符合自身業(yè)務(wù)的限流規(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獲取對應(yīng)匹配的前綴的限流配置
* @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:
# 默認(rèn)限流配置,如果前綴沒有匹配上,則使用默認(rèn)配置
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;
/**
* 嘗試獲取一個限流器許可,如果當(dāng)前沒有足夠的令牌,調(diào)用線程將被阻塞,直到有足夠的令牌
* @param key 限流器的唯一標(biāo)識
*/
public void acquire(String key) {
getRedissonRateLimiter(key).acquire();
}
/**
* 嘗試立即獲取一個限流器許可,如果當(dāng)前沒有足夠的令牌,立即返回 false
* @param key 限流器的唯一標(biāo)識
* @return 成功:true 失?。篺alse
*/
public boolean tryAcquire(String key) {
return getRedissonRateLimiter(key).tryAcquire();
}
/**
* 嘗試在指定時間內(nèi)獲取一個限流器許可,如果在指定時間內(nèi)沒有足夠的令牌,立即返回 false
* @param key 限流器的唯一標(biāo)識
* @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 限流器的唯一標(biāo)識
* @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()) {
// 如果當(dāng)前限流器的配置與配置文件不一致,說明服務(wù)器重啟過
rateLimiter.delete();
// 配置文件為準(zhǔn),重新設(shè)置
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 設(shè)置限流配置方法trySetRate
在使用限流器之前,都需要提前設(shè)置限流配置,否則限流代碼會報錯(后面會提到具體哪一行代碼)
跟進(jìn)RedissonRateLimiter的trySetRate方法,可以找到下圖中的方法,可見設(shè)置限流配置很簡單,只是一段很簡單的Lua腳本,在腳本中往Hash數(shù)據(jù)結(jié)構(gòu)中寫入了我們之前配置的限流參數(shù)。

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

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

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

再接著是{key}:permits,對應(yīng)著圖片中的{test}:permits,是一個有序集合,保存著限流過程中的訪問信息。有序集合中的value使用隨機(jī)數(shù)和你要獲取的token數(shù)量拼接作為value,防止value的重復(fù),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)
-- 如果配置沒有初始化(即獲取的值為空),拋出錯誤
-- 這里對應(yīng)著一開始要設(shè)置好限流配置參數(shù),否則Lua腳本執(zhí)行過程會拋出異常
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')
-- 當(dāng)前請求令牌數(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設(shè)置為10,在acqure的時候大于10,Lua腳本就會報錯
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount cannot exceed defined rate');
-- 獲取當(dāng)前剩余可用的令牌數(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;
-- 如果有過期的令牌,進(jìn)行釋放處理
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)整當(dāng)前令牌數(shù),確保不會超過桶的大小
currentValue = tonumber(rate) - used;
else
-- 否則,直接增加釋放的令牌數(shù)
currentValue = tonumber(currentValue) + released;
end;
-- 更新令牌數(shù)
redis.call('set', valueName, currentValue);
end;
-- 如果當(dāng)前令牌數(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
-- 如果當(dāng)前令牌數(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è)置當(dāng)前可用的令牌數(shù),添加訪問記錄,扣減可用令牌數(shù)量
else
-- 設(shè)置當(dāng)前可用的令牌數(shù)
redis.call('set', valueName, rate);
-- 新增領(lǐng)票訪問記錄
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不要設(shè)置過大
從Lua腳本中我們可以看到,Rate的值限制著有序集合中的限流訪問記錄,如果Rate值過大可能會導(dǎo)致有序集合中存儲的數(shù)據(jù)激增,從而Redis的內(nèi)存增加,并且使得Lua腳本的執(zhí)行效率下降,緊跟著就是限流功能的效率下降,傾向于小Rate+小時間窗口的方式,這種設(shè)置方式請求也會更均勻一些。
限流的上限取決于Redis的性能
RRateLimiter的限流能力受制于Redis實例的性能上限。例如,如果Redis實例的QPS上限是1w,那么通過RRateLimiter實現(xiàn)2w QPS限流是不可能的。要突破單個Redis實例性能的限制,可以通過拆分多個限流器來實現(xiàn)。具體做法是創(chuàng)建多個限流器,使用不同的名稱,并在各臺機(jī)器上隨機(jī)選擇一個限流器進(jìn)行限流,這樣總流量就可以被分散到多個限流器上,從而提升整體限流上限。
到此這篇關(guān)于詳解Redisson分布式限流的使用及原理的文章就介紹到這了,更多相關(guān)Redisson分布式限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Redis實現(xiàn)分布式限流的幾種方法
- redis+lua實現(xiàn)分布式限流的示例
- Redisson分布式限流的實現(xiàn)原理分析
- Redisson分布式限流器RRateLimiter的使用及原理小結(jié)
- Redis分布式限流的幾種實現(xiàn)
- Redisson分布式限流的實現(xiàn)原理解析
- redisson分布式限流RRateLimiter源碼解析
- Redis分布式限流組件設(shè)計與使用實例
- 基于Redis+Lua腳本實現(xiàn)分布式限流組件封裝的方法
- Redis和Lua實現(xiàn)分布式限流器的方法詳解
- Redis分布式限流生產(chǎn)環(huán)境落地方案
相關(guān)文章
Java將文件夾保留目錄打包為 ZIP 壓縮包并下載的教程詳解
這篇文章主要介紹了Java將文件夾保留目錄打包為 ZIP 壓縮包并下載的教程詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
Hibernate雙向一對一映射關(guān)系配置代碼實例
這篇文章主要介紹了Hibernate雙向一對一映射關(guān)系配置代碼實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10
MyBatis-Plus通用中等、大量數(shù)據(jù)分批查詢和處理方法
文章介紹MyBatis-Plus分頁查詢處理,通過函數(shù)式接口與Lambda表達(dá)式實現(xiàn)通用邏輯,方法抽象但功能強(qiáng)大,建議擴(kuò)展分批處理及流式查詢以優(yōu)化大數(shù)據(jù)量處理,感興趣的朋友一起看看吧2025-07-07
深入學(xué)習(xí)java位運算的基礎(chǔ)知識
位運算是直接對整數(shù)在內(nèi)存中的二進(jìn)制位進(jìn)行操作嗎,位運算即可以節(jié)約內(nèi)存,同時使程序速度更快效率更高。文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,下面我們來一起學(xué)習(xí)下吧2019-06-06
spring boot項目打包成war在tomcat運行的全步驟
這篇文章主要給大家介紹了關(guān)于spring boot項目打包成war在tomcat運行的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Java使用PrepareStatement實現(xiàn)數(shù)據(jù)的插入與查詢操作
這篇文章主要為大家詳細(xì)介紹了Java如何使用PrepareStatement實現(xiàn)數(shù)據(jù)的插入與查詢操作,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
Spring中BeanFactory和ApplicationContext的作用和區(qū)別(推薦)
這篇文章主要介紹了Spring中BeanFactory和ApplicationContext的作用和區(qū)別,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

