詳解Redisson分布式限流的使用及原理
1. 常見的分布式限流算法
1.1 固定窗口算法
原理:固定窗口算法是一種簡單的限流算法。在固定時間窗口內(nèi),記錄請求的數(shù)量。例如,設(shè)定每10秒鐘最多允許5次請求,如果在當前窗口內(nèi)的請求數(shù)超過限制,則拒絕請求。
優(yōu)點:簡單易實現(xiàn)。
缺點:存在“流量突刺”的問題,也就是在窗口切換時可能會產(chǎn)生兩倍于閾值流量的請求
適用場景: 適合于對于突發(fā)流量容忍度高的場景。
1.2 滑動窗口算法
原理: 滑動窗口算法在固定窗口的基礎(chǔ)上,將一個計時窗口分成了若干個小窗口,然后每個小窗口維護一個獨立的計數(shù)器。當請求的時間大于當前窗口的最大時間時,則將計時窗口向前平移一個小窗口。平移時,將第一個小窗口的數(shù)據(jù)丟棄,然后將第二個小窗口設(shè)置為第一個小窗口,同時在最后面新增一個小窗口,將新的請求放在新增的小窗口中。同時要保證整個窗口中所有小窗口的請求數(shù)目之后不能超過設(shè)定的閾值。
優(yōu)點: 相比滑動窗口,它能夠更精確地控制請求頻率,避免了“流量突刺”問題。
缺點: 滑動窗口算法是固定窗口的一種改進,但從根本上并沒有真正解決固定窗口算法的臨界突發(fā)流量問題。實現(xiàn)相對復(fù)雜,要求記錄每個時間窗口的請求數(shù),需要更多的存儲和計算資源。
適用場景: 比較適合對于不能容忍臨界突發(fā)流量的場景。
1.3 漏桶算法
原理: 漏桶算法將請求放入一個桶中,桶以固定速率漏出請求。如果桶滿了,新的請求將被丟棄。請求的流入速率可以不均勻,但漏出的速率是固定的,確保請求按照固定速率被處理。
優(yōu)點: 適用于需要平滑處理突發(fā)流量的場景,能夠把瞬時流量平滑到平穩(wěn)流量。
缺點: 請求的流入速率過快會導(dǎo)致請求丟失,需要合理設(shè)置桶的容量和漏水速率。不支持突發(fā)流量。
適用場景: 例如保護數(shù)據(jù)庫的限流,先把對數(shù)據(jù)庫的訪問加入到桶中,工作線程再以數(shù)據(jù)庫能夠承受的請求壓力從桶中取出請求,去訪問數(shù)據(jù)庫。
1.4 令牌桶算法
原理:令牌桶算法是對漏斗算法的一種改進,除了能夠起到限流的作用外,還允許一定程度的流量突發(fā)。在令牌桶算法中,存在一個令牌桶,算法中存在一種機制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果滿了令牌就無法放進去了。當請求來時,會首先到令牌桶中去拿令牌,如果拿到了令牌,則該請求會被處理,并消耗掉拿到的令牌;如果令牌桶為空,則該請求會被丟棄。
優(yōu)點: 允許突發(fā)流量,且能平滑處理請求流量。
缺點:對桶的大小和令牌生成速率要求較高,對存儲資源的需求較大,因為需要維護令牌桶。
適用場景: 適合電商搶購或者微博出現(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 提供的單機限流、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: # 默認限流配置,如果前綴沒有匹配上,則使用默認配置 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 失?。篺alse */ 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()) { // 如果當前限流器的配置與配置文件不一致,說明服務(wù)器重啟過 rateLimiter.delete(); // 配置文件為準,重新設(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è)置限流配置,否則限流代碼會報錯(后面會提到具體哪一行代碼)
跟進RedissonRateLimiter
的trySetRate
方法,可以找到下圖中的方法,可見設(shè)置限流配置很簡單,只是一段很簡單的Lua腳本,在腳本中往Hash
數(shù)據(jù)結(jié)構(gòu)中寫入了我們之前配置的限流參數(shù)。
3.2 acquire
限流方法原理
跟進RedissonRateLimiter
的acquire
方法,可以找到一段Lua腳本代碼,在展開講解Lua腳本之前,需要先了解一下限流需要用到哪些Redis鍵值
圖片中我使用的key是test:
首先是{key},對應(yīng)著圖片中的test,是哈希數(shù)據(jù)類型,里面存儲著限流配置,比如:rate、interval等信息,這里的keepAliveTime先不做考慮,與本次要講解的核心限流邏輯無太大關(guān)系。
接著是{key}:value,對應(yīng)著圖片中的{test}:value,是字符串數(shù)據(jù)類型,value是一個數(shù)字,表示可用的令牌桶數(shù)量。
再接著是{key}:permits,對應(yīng)著圖片中的{test}:permits,是一個有序集合,保存著限流過程中的訪問信息。有序集合中的value使用隨機數(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') -- 當前請求令牌數(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'); -- 獲取當前剩余可用的令牌數(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ù),添加訪問記錄,扣減可用令牌數(shù)量 else -- 設(shè)置當前可用的令牌數(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)建多個限流器,使用不同的名稱,并在各臺機器上隨機選擇一個限流器進行限流,這樣總流量就可以被分散到多個限流器上,從而提升整體限流上限。
到此這篇關(guān)于詳解Redisson分布式限流的使用及原理的文章就介紹到這了,更多相關(guān)Redisson分布式限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析SpringBoot中使用thymeleaf找不到.HTML文件的原因
這篇文章主要介紹了SpringBoot中使用thymeleaf找不到.HTML文件的原因分析,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進新的idea中
這篇文章主要介紹了換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進新的idea中,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11你知道怎么用Spring的三級緩存解決循環(huán)依賴嗎
這篇文章主要為大家詳細介紹了Spring的三級緩存解決循環(huán)依賴,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02利用HttpUrlConnection 上傳 接收文件的實現(xiàn)方法
下面小編就為大家?guī)硪黄肏ttpUrlConnection 上傳 接收文件的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11java線程并發(fā)控制同步工具CountDownLatch
這篇文章主要為大家介紹了java線程并發(fā)控制同步工具CountDownLatch使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版)
這篇文章主要介紹了SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10