最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

詳解Redis如何多規(guī)則限流和防重復提交

 更新時間:2023年12月15日 09:31:29   作者:翼飛  
市面上很多介紹redis如何實現(xiàn)限流的,但是大部分都有一個缺點,就是只能實現(xiàn)單一的限流,但是如果想一個接口兩種規(guī)則都需要滿足呢,使用本文就來介紹一下redis實現(xiàn)分布式多規(guī)則限流的方式吧

一、簡介

市面上很多介紹redis如何實現(xiàn)限流的,但是大部分都有一個缺點,就是只能實現(xiàn)單一的限流,比如1分鐘訪問1次或者60分鐘訪問10次這種,但是如果想一個接口兩種規(guī)則都需要滿足呢,我們的項目又是分布式項目,應(yīng)該如何解決,下面就介紹一下redis實現(xiàn)分布式多規(guī)則限流的方式。

二、思考

  • 如何一分鐘只能發(fā)送一次驗證碼,一小時只能發(fā)送10次驗證碼等等多種規(guī)則的限流
  • 如何防止接口被惡意打擊(短時間內(nèi)大量請求)
  • 如何限制接口規(guī)定時間內(nèi)訪問次數(shù)

三、解決方法

1. 使用 String結(jié)構(gòu) 記錄固定時間段內(nèi)某用戶IP訪問某接口的次數(shù)

  • RedisKey = prefix : className : methodName
  • RedisVlue = 訪問次數(shù)

攔截請求:

  • 初次訪問時設(shè)置 [RedisKey] [RedisValue=1] [規(guī)定的過期時間]
  • 獲取 RedisValue 是否超過規(guī)定次數(shù),超過則攔截,未超過則對 RedisKey 進行加1

分析: 規(guī)則是每分鐘訪問 1000 次

1.考慮并發(fā)問題

  • 假設(shè)目前 RedisKey => RedisValue 為 999
  • 目前大量請求進行到第一步( 獲取Redis請求次數(shù) ),那么所有線程都獲取到了值為999,進行判斷都未超過限定次數(shù)則不攔截,導致實際次數(shù)超過 1000 次
  • 解決辦法: 保證方法執(zhí)行原子性(加鎖、lua)

2.考慮在臨界值進行訪問

思考下圖

代碼實現(xiàn): 比較簡單(可參考若依代碼 gitee.com/y_project/RuoYi-Vue/blob/master/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java

2. 使用 Zset 進行存儲,解決臨界值訪問問題

網(wǎng)上幾乎都有實現(xiàn),這里就不過多介紹

3.實現(xiàn)多規(guī)則限流

3.1 先確定最終需要的效果

  • 能實現(xiàn)多種限流規(guī)則
  • 能實現(xiàn)防重復提交

通過以上要求設(shè)計注解(先想象出最終實現(xiàn)效果)

@RateLimiter(
        rules = {
                // 60秒內(nèi)只能訪問10次
                @RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS),
                // 120秒內(nèi)只能訪問20次
                @RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS)

        },
        // 防重復提交 (5秒鐘只能訪問1次)
        preventDuplicate = true
)

3.2 編寫注解(RateLimiter,RateRule)

編寫 RateLimiter 注解

/**
 * @Description: 請求接口限制
 * @Author: yiFei
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateLimiter {

    /**
     * 限流key
     */
    String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;

    /**
     * 限流類型 ( 默認 Ip 模式 )
     */
    LimitTypeEnum limitType() default LimitTypeEnum.IP;

    /**
     * 錯誤提示
     */
    ResultCode message() default ResultCode.REQUEST_MORE_ERROR;

    /**
     * 限流規(guī)則 (規(guī)則不可變,可多規(guī)則)
     */
    RateRule[] rules() default {};

    /**
     * 防重復提交值
     */
    boolean preventDuplicate() default false;

    /**
     * 防重復提交默認值
     */
    RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);
}

編寫RateRule注解

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateRule {

    /**
     * 限流次數(shù)
     */
    long count() default 10;

    /**
     * 限流時間
     */
    long time() default 60;

    /**
     * 限流時間單位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

}

3.3 攔截注解 RateLimiter

確定redis存儲方式

  • RedisKey = prefix : className : methodName
  • RedisScore = 時間戳
  • RedisValue = 任意分布式不重復的值即可

編寫生成 RedisKey 的方法

/**
 * 通過 rateLimiter 和 joinPoint 拼接  prefix : ip / userId : classSimpleName - methodName
 *
 * @param rateLimiter 提供 prefix
 * @param joinPoint   提供 classSimpleName : methodName
 * @return
 */
public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {
    StringBuffer key = new StringBuffer(rateLimiter.key());
    // 不同限流類型使用不同的前綴
    switch (rateLimiter.limitType()) {
        // XXX 可以新增通過參數(shù)指定參數(shù)進行限流
        case IP:
            key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");
            break;
        case USER_ID:
            SysUserDetails user = SecurityUtil.getUser();
            if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":");
            break;
        case GLOBAL:
            break;
    }
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    Class<?> targetClass = method.getDeclaringClass();
    key.append(targetClass.getSimpleName()).append("-").append(method.getName());
    return key.toString();
}

3.4 編寫lua腳本 (兩種將時間添加到Redis的方法)

3.4.1 UUID(可用其他有相同的特性的值)為Zset中的value值

參數(shù)介紹

  • KEYS[1] = prefix : ? : className : methodName
  • KEYS[2] = 唯一ID
  • KEYS[3] = 當前時間
  • ARGV = [次數(shù),單位時間,次數(shù),單位時間, 次數(shù), 單位時間 ...]

由java傳入分布式不重復的 value 值

-- 1. 獲取參數(shù)
local key = KEYS[1]
local uuid = KEYS[2]
local currentTime = tonumber(KEYS[3])
-- 2. 以數(shù)組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數(shù)組查看是否超過限流規(guī)則
for i = 1, #ARGV, 2 do
    local rateRuleCount = tonumber(ARGV[i])
    local rateRuleTime = tonumber(ARGV[i + 1])
    -- 3.1 判斷在單位時間內(nèi)訪問次數(shù)
    local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
    -- 3.2 判斷是否超過規(guī)定次數(shù)
    if tonumber(count) >= rateRuleCount then
        return true
    end
    -- 3.3 判斷元素最大值,設(shè)置為最終過期時間
    if rateRuleTime > expireTime then
        expireTime = rateRuleTime
    end
end
-- 4. redis 中添加當前時間
redis.call('ZADD', key, currentTime, uuid)
-- 5. 更新緩存過期時間
redis.call('PEXPIRE', key, expireTime)
-- 6. 刪除最大時間限度之前的數(shù)據(jù),防止數(shù)據(jù)過多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
return false

3.4.2 根據(jù)時間戳作為Zset中的value值

參數(shù)介紹

  • KEYS[1] = prefix : ? : className : methodName
  • KEYS[2] = 當前時間
  • ARGV = [次數(shù),單位時間,次數(shù),單位時間, 次數(shù), 單位時間 ...]

根據(jù)時間進行生成value值,考慮同一毫秒添加相同時間值問題

以下為第二種實現(xiàn)方式,在并發(fā)高的情況下效率低,value是通過時間戳進行添加,但是訪問量大的話會使得一直在調(diào)用 redis.call('ZADD', key, currentTime, currentTime),但是在不沖突value的情況下,會比生成 UUID 好

-- 1. 獲取參數(shù)
local key = KEYS[1]
local currentTime = KEYS[2]
-- 2. 以數(shù)組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數(shù)組查看是否越界
for i = 1, #ARGV, 2 do
    local rateRuleCount = tonumber(ARGV[i])
    local rateRuleTime = tonumber(ARGV[i + 1])
    -- 3.1 判斷在單位時間內(nèi)訪問次數(shù)
    local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
    -- 3.2 判斷是否超過規(guī)定次數(shù)
    if tonumber(count) >= rateRuleCount then
        return true
    end
    -- 3.3 判斷元素最大值,設(shè)置為最終過期時間
    if rateRuleTime > expireTime then
        expireTime = rateRuleTime
    end
end
-- 4. 更新緩存過期時間
redis.call('PEXPIRE', key, expireTime)
-- 5. 刪除最大時間限度之前的數(shù)據(jù),防止數(shù)據(jù)過多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
-- 6. redis 中添加當前時間  ( 解決多個線程在同一毫秒添加相同 value 導致 Redis 漏記的問題 )
-- 6.1 maxRetries 最大重試次數(shù) retries 重試次數(shù)
local maxRetries = 5
local retries = 0
while true do
    local result = redis.call('ZADD', key, currentTime, currentTime)
    if result == 1 then
        -- 6.2 添加成功則跳出循環(huán)
        break
    else
        -- 6.3 未添加成功則 value + 1 再次進行嘗試
        retries = retries + 1
        if retries >= maxRetries then
            -- 6.4 超過最大嘗試次數(shù) 采用添加隨機數(shù)策略
            local random_value = math.random(1, 1000)
            currentTime = currentTime + random_value
        else
            currentTime = currentTime + 1
        end
    end
end

return false

3.5 編寫 AOP 攔截

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private RedisScript<Boolean> limitScript;

/**
 * 限流
 * XXX 對限流要求比較高,可以使用在 Redis中對規(guī)則進行存儲校驗 或者使用中間件
 *
 * @param joinPoint   joinPoint
 * @param rateLimiter 限流注解
 */
@Before(value = "@annotation(rateLimiter)")
public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) {
    // 1. 生成 key
    String key = getCombineKey(rateLimiter, joinPoint);
    try {
        // 2. 執(zhí)行腳本返回是否限流
        Boolean flag = redisTemplate.execute(limitScript,
                ListUtil.of(key, String.valueOf(System.currentTimeMillis())),
                (Object[]) getRules(rateLimiter));
        // 3. 判斷是否限流
        if (Boolean.TRUE.equals(flag)) {
            log.error("ip: '{}' 攔截到一個請求 RedisKey: '{}'",
                    IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),
                    key);
            throw new ServiceException(rateLimiter.message());
        }
    } catch (ServiceException e) {
        throw e;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 獲取規(guī)則
 *
 * @param rateLimiter 獲取其中規(guī)則信息
 * @return
 */
private Long[] getRules(RateLimiter rateLimiter) {
    int capacity = rateLimiter.rules().length << 1;
    // 1. 構(gòu)建 args
    Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];
    // 3. 記錄數(shù)組元素
    int index = 0;
    // 2. 判斷是否需要添加防重復提交到redis進行校驗
    if (rateLimiter.preventDuplicate()) {
        RateRule preventRateRule = rateLimiter.preventDuplicateRule();
        args[index++] = preventRateRule.count();
        args[index++] = preventRateRule.timeUnit().toMillis(preventRateRule.time());
    }
    RateRule[] rules = rateLimiter.rules();
    for (RateRule rule : rules) {
        args[index++] = rule.count();
        args[index++] = rule.timeUnit().toMillis(rule.time());
    }
    return args;
}

java代碼 RateLimiterAspect.java

以上就是詳解Redis如何多規(guī)則限流和防重復提交的詳細內(nèi)容,更多關(guān)于Redis多規(guī)則限流的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Redis 鍵空間事件通知的具體使用

    Redis 鍵空間事件通知的具體使用

    本文系統(tǒng)解析Redis鍵空間通知機制,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-11-11
  • redis分布式設(shè)計的實現(xiàn)示例

    redis分布式設(shè)計的實現(xiàn)示例

    Redis 的分布式設(shè)計主要通過三種模式實現(xiàn),每種模式解決不同的問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2026-05-05
  • redis 用scan指令 代替keys指令(詳解)

    redis 用scan指令 代替keys指令(詳解)

    下面小編就為大家分享一篇redis 用scan指令 代替keys指令詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-12-12
  • Redis 實現(xiàn)消息隊列實際案例

    Redis 實現(xiàn)消息隊列實際案例

    文章探討Redis作為消息隊列的三大核心方案(List、Pub/Sub、Stream)及適用場景,分析了輕量部署、高性能、多語言支持等優(yōu)勢,指出Stream在可靠性、消息確認、死信隊列等企業(yè)級需求上表現(xiàn)最佳,適合電商等高吞吐場景,并提供了實際應(yīng)用案例與優(yōu)化建議,感興趣的朋友一起看看吧
    2025-09-09
  • 使用Redis實現(xiàn)向量相似度搜索

    使用Redis實現(xiàn)向量相似度搜索

    在自然語言處理領(lǐng)域,有一個常見且重要的任務(wù)就是文本相似度搜索,所以本文為大家介紹一下如何利用Redis實現(xiàn)向量相似度搜索,解決文本、圖像和音頻之間的相似度匹配問題,需要的可以了解下
    2023-07-07
  • Redis大Key(Bigkey)問題識別與解決全解析

    Redis大Key(Bigkey)問題識別與解決全解析

    在高并發(fā)場景下,Redis 以極致的內(nèi)存操作速度成為緩存與NoSQL領(lǐng)域的首選,但隨著業(yè)務(wù)發(fā)展,大Key(Bigkey) 問題逐漸顯現(xiàn),帶來內(nèi)存風險、性能瓶頸、集群失衡等隱患,本文給大家介紹了Redis大Key(Bigkey)問題識別與解決,需要的朋友可以參考下
    2025-08-08
  • Redis分布式鎖如何設(shè)置超時時間

    Redis分布式鎖如何設(shè)置超時時間

    這篇文章主要介紹了Redis分布式鎖如何設(shè)置超時時間,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • 如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)

    如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)

    測試過程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測試過程中經(jīng)常有人會清空redis,回歸測試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下
    2024-03-03
  • Redis源碼分析之set?和?sorted?set?使用

    Redis源碼分析之set?和?sorted?set?使用

    本文介紹了Redis?中的?set?和?sorted?set?使用源碼實現(xiàn)分析,Redis?的?Set?是?String?類型的無序集合,集合成員是唯一的,sorted?set有序集合和集合一樣也是?string?類型元素的集合,對Redis?set?和?sorted?set使用相關(guān)知識感興趣的朋友一起看看吧
    2022-03-03
  • Redis + Lua 實現(xiàn)高性能分布式限流的詳細過程

    Redis + Lua 實現(xiàn)高性能分布式限流的詳細過程

    這篇文章給大家介紹Redis + Lua 實現(xiàn)高性能分布式限流的相關(guān)知識,本文將從原理出發(fā),深入解析令牌桶算法的核心邏輯,并結(jié)合生產(chǎn)級代碼實現(xiàn),帶你從零搭建一個支持多維度組合的分布式限流系統(tǒng),感興趣的朋友一起看看吧
    2026-05-05

最新評論

莆田市| 郎溪县| 江油市| 都兰县| 九江县| 平果县| 句容市| 隆安县| 静乐县| 崇阳县| 雷山县| 双柏县| 大名县| 涞源县| 台江县| 麟游县| 弋阳县| 搜索| 师宗县| 东安县| 左云县| 渝北区| 遂昌县| 黔西| 雷波县| 介休市| 镇原县| 涿州市| 庄河市| 绥化市| 维西| 祁连县| 蓬莱市| 专栏| 宣汉县| 大兴区| 甘孜县| 宣化县| 杨浦区| 平阴县| 金阳县|