欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Redisson分布式限流的使用及原理

 更新時間:2025年02月21日 10:37:26   作者:重生之Java再愛我一次  
本文介紹了Redisson分布式限流的使用方法和原理,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

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)接口(如 RMapRSetRLock 等),并支持多種部署模式(單節(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è)置限流配置,否則限流代碼會報錯(后面會提到具體哪一行代碼)

跟進RedissonRateLimitertrySetRate方法,可以找到下圖中的方法,可見設(shè)置限流配置很簡單,只是一段很簡單的Lua腳本,在腳本中往Hash數(shù)據(jù)結(jié)構(gòu)中寫入了我們之前配置的限流參數(shù)。

在這里插入圖片描述

3.2 acquire限流方法原理

跟進RedissonRateLimiteracquire方法,可以找到一段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)文章

  • JAVA并發(fā)圖解

    JAVA并發(fā)圖解

    這篇文章主要介紹了JAVA的并發(fā),文中圖解非常細致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2021-09-09
  • SpringMVC post請求中文亂碼問題解決

    SpringMVC post請求中文亂碼問題解決

    這篇文章主要介紹了SpringMVC post請求中文亂碼問題解決,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-12-12
  • SpringBoot整合MongoDB全過程

    SpringBoot整合MongoDB全過程

    這篇文章主要介紹了SpringBoot整合MongoDB全過程,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-05-05
  • 淺析SpringBoot中使用thymeleaf找不到.HTML文件的原因

    淺析SpringBoot中使用thymeleaf找不到.HTML文件的原因

    這篇文章主要介紹了SpringBoot中使用thymeleaf找不到.HTML文件的原因分析,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-07-07
  • 換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進新的idea中

    換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進新的idea中

    這篇文章主要介紹了換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進新的idea中,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-11-11
  • 你知道怎么用Spring的三級緩存解決循環(huán)依賴嗎

    你知道怎么用Spring的三級緩存解決循環(huán)依賴嗎

    這篇文章主要為大家詳細介紹了Spring的三級緩存解決循環(huán)依賴,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-02-02
  • 利用HttpUrlConnection 上傳 接收文件的實現(xiàn)方法

    利用HttpUrlConnection 上傳 接收文件的實現(xiàn)方法

    下面小編就為大家?guī)硪黄肏ttpUrlConnection 上傳 接收文件的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-11-11
  • Java 對稱加密幾種算法分別實現(xiàn)

    Java 對稱加密幾種算法分別實現(xiàn)

    這篇文章主要介紹了Java 對稱加密使用DES / 3DES / AES 這三種算法分別實現(xiàn)的相關(guān)資料,這里提供了實例代碼,需要的朋友可以參考下
    2017-01-01
  • java線程并發(fā)控制同步工具CountDownLatch

    java線程并發(fā)控制同步工具CountDownLatch

    這篇文章主要為大家介紹了java線程并發(fā)控制同步工具CountDownLatch使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版)

    SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版)

    這篇文章主要介紹了SpringBoot結(jié)合ProGuard實現(xiàn)代碼混淆(最新版),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-10-10

最新評論