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

基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流的步驟

 更新時(shí)間:2024年12月31日 09:01:05   作者:廢物大師兄  
滑動(dòng)窗口算法是一種基于時(shí)間窗口的限流算法,通過(guò)動(dòng)態(tài)地滑動(dòng)窗口,可以動(dòng)態(tài)調(diào)整限流的速率,Redis有序集合可以用來(lái)實(shí)現(xiàn)滑動(dòng)窗口限流,本文介紹基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流,感興趣的朋友一起看看吧

滑動(dòng)窗口算法是一種基于時(shí)間窗口的限流算法,它將時(shí)間劃分為若干個(gè)固定大小的窗口,每個(gè)窗口內(nèi)記錄了該時(shí)間段內(nèi)的請(qǐng)求次數(shù)。通過(guò)動(dòng)態(tài)地滑動(dòng)窗口,可以動(dòng)態(tài)調(diào)整限流的速率,以應(yīng)對(duì)不同的流量變化。

整個(gè)限流可以概括為兩個(gè)主要步驟:

  • 統(tǒng)計(jì)窗口內(nèi)的請(qǐng)求數(shù)量
  • 應(yīng)用限流規(guī)則

Redis有序集合每個(gè)value有一個(gè)score(分?jǐn)?shù)),基于score我們可以定義一個(gè)時(shí)間窗口,然后每次一個(gè)請(qǐng)求進(jìn)來(lái)就設(shè)置一個(gè)value,這樣就可以統(tǒng)計(jì)窗口內(nèi)的請(qǐng)求數(shù)量。key可以是資源名,比如一個(gè)url,或者ip+url,用戶標(biāo)識(shí)+url等。value在這里不那么重要,因?yàn)槲覀冎恍枰y(tǒng)計(jì)數(shù)量,因此value可以就設(shè)置成時(shí)間戳,但是如果value相同的話就會(huì)被覆蓋,所以我們可以把請(qǐng)求的數(shù)據(jù)做一個(gè)hash,將這個(gè)hash值當(dāng)value,或者如果每個(gè)請(qǐng)求有流水號(hào)的話,可以用請(qǐng)求流水號(hào)當(dāng)value,總之就是要能唯一標(biāo)識(shí)一次請(qǐng)求的。

所以,簡(jiǎn)化后的命令就變成了:

ZADD  資源標(biāo)識(shí)   時(shí)間戳   請(qǐng)求標(biāo)識(shí)

public boolean isAllow(String key) {
    ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
    //  獲取當(dāng)前時(shí)間戳
    long currentTime = System.currentTimeMillis();
    //  當(dāng)前時(shí)間 - 窗口大小 = 窗口開(kāi)始時(shí)間
    long windowStart = currentTime - period;
    //  刪除窗口開(kāi)始時(shí)間之前的所有數(shù)據(jù)
    zSetOperations.removeRangeByScore(key, 0, windowStart);
    //  統(tǒng)計(jì)窗口中請(qǐng)求數(shù)量
    Long count = zSetOperations.zCard(key);
    //  如果窗口中已經(jīng)請(qǐng)求的數(shù)量超過(guò)閾值,則直接拒絕
    if (count >= threshold) {
        return false;
    }
    //  沒(méi)有超過(guò)閾值,則加入集合
    String value = "請(qǐng)求唯一標(biāo)識(shí)(比如:請(qǐng)求流水號(hào)、哈希值、MD5值等)";
    zSetOperations.add(key, String.valueOf(currentTime), currentTime);
    //  設(shè)置一個(gè)過(guò)期時(shí)間,及時(shí)清理冷數(shù)據(jù)
    stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
    //  通過(guò)
    return true;
}

上面代碼中涉及到三條Redis命令,并發(fā)請(qǐng)求下可能存在問(wèn)題,所以我們把它們寫(xiě)成Lua腳本

local key = KEYS[1]
local current_time = tonumber(ARGV[1])
local window_size = tonumber(ARGV[2])
local threshold = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)
local count = redis.call('ZCARD', key)
if count >= threshold then
    return tostring(0)
else
    redis.call('ZADD', key, tostring(current_time), current_time)
    return tostring(1)
end

完整的代碼如下:

package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
 * 基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Service
public class SlidingWindowRatelimiter {
    private long period = 60*1000;  //  1分鐘
    private int threshold = 3;      //  3次
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * RedisTemplate
     */
    public boolean isAllow(String key) {
        ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
        //  獲取當(dāng)前時(shí)間戳
        long currentTime = System.currentTimeMillis();
        //  當(dāng)前時(shí)間 - 窗口大小 = 窗口開(kāi)始時(shí)間
        long windowStart = currentTime - period;
        //  刪除窗口開(kāi)始時(shí)間之前的所有數(shù)據(jù)
        zSetOperations.removeRangeByScore(key, 0, windowStart);
        //  統(tǒng)計(jì)窗口中請(qǐng)求數(shù)量
        Long count = zSetOperations.zCard(key);
        //  如果窗口中已經(jīng)請(qǐng)求的數(shù)量超過(guò)閾值,則直接拒絕
        if (count >= threshold) {
            return false;
        }
        //  沒(méi)有超過(guò)閾值,則加入集合
        String value = "請(qǐng)求唯一標(biāo)識(shí)(比如:請(qǐng)求流水號(hào)、哈希值、MD5值等)";
        zSetOperations.add(key, String.valueOf(currentTime), currentTime);
        //  設(shè)置一個(gè)過(guò)期時(shí)間,及時(shí)清理冷數(shù)據(jù)
        stringRedisTemplate.expire(key, period, TimeUnit.MILLISECONDS);
        //  通過(guò)
        return true;
    }
    /**
     * Lua腳本
     */
    public boolean isAllow2(String key) {
        String luaScript = "local key = KEYS[1]\n" +
                "local current_time = tonumber(ARGV[1])\n" +
                "local window_size = tonumber(ARGV[2])\n" +
                "local threshold = tonumber(ARGV[3])\n" +
                "redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)\n" +
                "local count = redis.call('ZCARD', key)\n" +
                "if count >= threshold then\n" +
                "    return tostring(0)\n" +
                "else\n" +
                "    redis.call('ZADD', key, tostring(current_time), current_time)\n" +
                "    return tostring(1)\n" +
                "end";
        long currentTime = System.currentTimeMillis();
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        String result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(currentTime), String.valueOf(period), String.valueOf(threshold));
        //  返回1表示通過(guò),返回0表示拒絕
        return "1".equals(result);
    }
}

這里用StringRedisTemplate執(zhí)行Lua腳本,先把Lua腳本封裝成DefaultRedisScript對(duì)象。注意,千萬(wàn)注意,Lua腳本的返回值必須是字符串,參數(shù)也最好都是字符串,用整型的話可能類(lèi)型轉(zhuǎn)換錯(cuò)誤。

String requestId = UUID.randomUUID().toString();
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
String result = stringRedisTemplate.execute(redisScript,
        Collections.singletonList(key),
        requestId,
        String.valueOf(period),
        String.valueOf(threshold));

好了,上面就是基于Redis有序集合實(shí)現(xiàn)的滑動(dòng)窗口限流。順帶提一句,Redis List類(lèi)型也可以用來(lái)實(shí)現(xiàn)滑動(dòng)窗口。

接下來(lái),我們來(lái)完善一下上面的代碼,通過(guò)AOP來(lái)攔截請(qǐng)求達(dá)到限流的目的

為此,我們必須自定義注解,然后根據(jù)注解參數(shù),來(lái)個(gè)性化的控制限流。那么,問(wèn)題來(lái)了,如果獲取注解參數(shù)呢?

舉例說(shuō)明:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value();
}
@Aspect
@Component
public class MyAspect {
    @Before("@annotation(myAnnotation)")
    public void beforeMethod(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        // 獲取注解參數(shù)
        String value = myAnnotation.value();
        System.out.println("Annotation value: " + value);
        // 其他業(yè)務(wù)邏輯...
    }
}

注意看,切點(diǎn)是怎么寫(xiě)的 @Before("@annotation(myAnnotation)")

是@Before("@annotation(myAnnotation)"),而不是@Before("@annotation(MyAnnotation)")

myAnnotation,是參數(shù),而MyAnnotation則是注解類(lèi)

此處參考資料

https://www.cnblogs.com/javaxubo/p/16556924.html

https://blog.csdn.net/qq_40977118/article/details/119488358

https://blog.51cto.com/knifeedge/5529885

言歸正傳,我們首先定義一個(gè)注解

package com.example.demo.controller;
import java.lang.annotation.*;
/**
 * 請(qǐng)求速率限制
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    /**
     * 窗口大?。J(rèn):60秒)
     */
    long period() default 60;
    /**
     * 閾值(默認(rèn):3次)
     */
    long threshold() default 3;
}

定義切面

package com.example.demo.controller;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.util.concurrent.TimeUnit;
/**
 * @Author: ChengJianSheng
 * @Date: 2024/12/26
 */
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
//    @Autowired
//    private SlidingWindowRatelimiter slidingWindowRatelimiter;
    @Before("@annotation(rateLimit)")
    public void doBefore(JoinPoint joinPoint, RateLimit rateLimit) {
        //  獲取注解參數(shù)
        long period = rateLimit.period();
        long threshold = rateLimit.threshold();
        //  獲取請(qǐng)求信息
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        String uri = httpServletRequest.getRequestURI();
        Long userId = 123L;     //  模擬獲取用戶ID
        String key = "limit:" + userId + ":" + uri;
        /*
        if (!slidingWindowRatelimiter.isAllow2(key)) {
            log.warn("請(qǐng)求超過(guò)速率限制!userId={}, uri={}", userId, uri);
            throw new RuntimeException("請(qǐng)求過(guò)于頻繁!");
        }*/
        ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
        //  獲取當(dāng)前時(shí)間戳
        long currentTime = System.currentTimeMillis();
        //  當(dāng)前時(shí)間 - 窗口大小 = 窗口開(kāi)始時(shí)間
        long windowStart = currentTime - period * 1000;
        //  刪除窗口開(kāi)始時(shí)間之前的所有數(shù)據(jù)
        zSetOperations.removeRangeByScore(key, 0, windowStart);
        //  統(tǒng)計(jì)窗口中請(qǐng)求數(shù)量
        Long count = zSetOperations.zCard(key);
        //  如果窗口中已經(jīng)請(qǐng)求的數(shù)量超過(guò)閾值,則直接拒絕
        if (count < threshold) {
            //  沒(méi)有超過(guò)閾值,則加入集合
            zSetOperations.add(key, String.valueOf(currentTime), currentTime);
            //  設(shè)置一個(gè)過(guò)期時(shí)間,及時(shí)清理冷數(shù)據(jù)
            stringRedisTemplate.expire(key, period, TimeUnit.SECONDS);
        } else {
            throw new RuntimeException("請(qǐng)求過(guò)于頻繁!");
        }
    }
}

加注解

@RestController
@RequestMapping("/hello")
public class HelloController {
    @RateLimit(period = 30, threshold = 2)
    @GetMapping("/sayHi")
    public void sayHi() {
    }
}

最后,看Redis中的數(shù)據(jù)結(jié)構(gòu)

最后的最后,流量控制建議看看阿里巴巴 Sentinel

https://sentinelguard.io/zh-cn/

到此這篇關(guān)于基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流的文章就介紹到這了,更多相關(guān)基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis特殊數(shù)據(jù)類(lèi)型bitmap位圖

    Redis特殊數(shù)據(jù)類(lèi)型bitmap位圖

    這篇文章主要為大家介紹了Redis特殊數(shù)據(jù)類(lèi)型bitmap位圖,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-05-05
  • 解讀redis?slaveof命令執(zhí)行后為什么需要清庫(kù)重新同步

    解讀redis?slaveof命令執(zhí)行后為什么需要清庫(kù)重新同步

    這篇文章主要介紹了redis?slaveof命令執(zhí)行后為什么需要清庫(kù)重新同步,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • redis和rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的示例代碼

    redis和rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列的示例代碼

    在高并發(fā)場(chǎng)景下,延遲隊(duì)列顯得尤為重要,本文主要介紹了兩種方式,redis和rabbitmq實(shí)現(xiàn)延時(shí)隊(duì)列,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-03-03
  • redis的hGetAll函數(shù)的性能問(wèn)題(記Redis那坑人的HGETALL)

    redis的hGetAll函數(shù)的性能問(wèn)題(記Redis那坑人的HGETALL)

    這篇文章主要介紹了redis的hGetAll函數(shù)的性能問(wèn)題,需要的朋友可以參考下
    2016-02-02
  • Redis AOF文件損壞報(bào)錯(cuò)的完整修復(fù)方案

    Redis AOF文件損壞報(bào)錯(cuò)的完整修復(fù)方案

    文章提供了三種修復(fù)Redis AOF持久化文件損壞的方法,方法一:備份損壞文件,使用官方工具修復(fù);方法二:修改配置讓Redis忽略損壞的文件尾部;方法三:如果是Redis且混合AOF格式,使用相同修復(fù)命令,每種方法都有詳細(xì)步驟,確保修復(fù)后Redis可以正常啟動(dòng),需要的朋友可以參考下
    2026-04-04
  • Redis 分片集群的實(shí)現(xiàn)

    Redis 分片集群的實(shí)現(xiàn)

    本文主要介紹了Redis 分片集群的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • 封裝Redis工具類(lèi)實(shí)踐

    封裝Redis工具類(lèi)實(shí)踐

    本文介紹了基于StringRedisTemplate封裝的Redis工具類(lèi),該工具類(lèi)提供了多種方法來(lái)處理緩存,包括設(shè)置TTL過(guò)期時(shí)間、邏輯過(guò)期時(shí)間、處理緩存擊穿和穿透問(wèn)題,在ShopServiceImpl中調(diào)用了這些方法
    2025-12-12
  • Redis使用Bitmap的方法實(shí)現(xiàn)

    Redis使用Bitmap的方法實(shí)現(xiàn)

    本文主要介紹了Redis使用Bitmap的方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • 解析Redis Cluster原理

    解析Redis Cluster原理

    redis最開(kāi)始使用主從模式做集群,若master宕機(jī)需要手動(dòng)配置slave轉(zhuǎn)為master;后來(lái)為了高可用提出來(lái)哨兵模式,該模式下有一個(gè)哨兵監(jiān)視master和slave,若master宕機(jī)可自動(dòng)將slave轉(zhuǎn)為master,但它也有一個(gè)問(wèn)題,就是不能動(dòng)態(tài)擴(kuò)充;所以在3.x提出cluster集群模式
    2021-06-06
  • redis中全局命令exists、del、expire、ttl(惰性刪除和定期刪除)

    redis中全局命令exists、del、expire、ttl(惰性刪除和定期刪除)

    本文主要介紹了redis中全局命令exists、del、expire、ttl(惰性刪除和定期刪除),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2025-11-11

最新評(píng)論

贵州省| 中超| 平塘县| 金平| 石柱| 黄石市| 宣威市| 海安县| 峨眉山市| 通州市| 封丘县| 黄平县| 扬中市| 饶平县| 峨眉山市| 关岭| 东丰县| 奉贤区| 肇东市| 祁阳县| 独山县| 新丰县| 岳阳县| 泉州市| 巩义市| 兴业县| 凤凰县| 沭阳县| 手游| 城市| 灵台县| 南华县| 双桥区| 凤凰县| 武强县| 永平县| 德格县| 合肥市| 诸暨市| 都匀市| 辽中县|