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

SpringBoot使用Redis對(duì)用戶(hù)IP進(jìn)行接口限流的項(xiàng)目實(shí)踐

 更新時(shí)間:2023年07月28日 15:28:45   作者:莫輕言舞  
本文主要介紹了SpringBoot使用Redis對(duì)用戶(hù)IP進(jìn)行接口限流,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

一、思路

使用接口限流的主要目的在于提高系統(tǒng)的穩(wěn)定性,防止接口被惡意打擊(短時(shí)間內(nèi)大量請(qǐng)求)。

比如要求某接口在1分鐘內(nèi)請(qǐng)求次數(shù)不超過(guò)1000次,那么應(yīng)該如何設(shè)計(jì)代碼呢?

下面講兩種思路,如果想看代碼可直接翻到后面的代碼部分。

1.1 固定時(shí)間段(舊思路)

1.1.1 思路描述

該方案的思路是:使用Redis記錄固定時(shí)間段內(nèi)某用戶(hù)IP訪(fǎng)問(wèn)某接口的次數(shù),其中:

  • Redis的key:用戶(hù)IP + 接口方法名
  • Redis的value:當(dāng)前接口訪(fǎng)問(wèn)次數(shù)。

當(dāng)用戶(hù)在近期內(nèi)第一次訪(fǎng)問(wèn)該接口時(shí),向Redis中設(shè)置一個(gè)包含了用戶(hù)IP和接口方法名的key,value的值初始化為1(表示第一次訪(fǎng)問(wèn)當(dāng)前接口)。同時(shí),設(shè)置該key的過(guò)期時(shí)間(比如為60秒)。

之后,只要這個(gè)key還未過(guò)期,用戶(hù)每次訪(fǎng)問(wèn)該接口都會(huì)導(dǎo)致value自增1次。

用戶(hù)每次訪(fǎng)問(wèn)接口前,先從Redis中拿到當(dāng)前接口訪(fǎng)問(wèn)次數(shù),如果發(fā)現(xiàn)訪(fǎng)問(wèn)次數(shù)大于規(guī)定的次數(shù)(如超過(guò)1000次),則向用戶(hù)返回接口訪(fǎng)問(wèn)失敗的標(biāo)識(shí)。

圖片

1.1.2 思路缺陷

該方案的缺點(diǎn)在于,限流時(shí)間段是固定的。

比如要求某接口在1分鐘內(nèi)請(qǐng)求次數(shù)不超過(guò)1000次,觀察以下流程:

圖片

圖片

可以發(fā)現(xiàn),00:59和01:01之間僅僅間隔了2秒,但接口卻被訪(fǎng)問(wèn)了1000+999=1999次,是限流次數(shù)(1000次)的2倍!

所以在該方案中,限流次數(shù)的設(shè)置可能不起作用,仍然可能在短時(shí)間內(nèi)造成大量訪(fǎng)問(wèn)。

1.2 滑動(dòng)窗口(新思路)

1.2.1 思路描述

為了避免出現(xiàn)方案1中由于鍵過(guò)期導(dǎo)致的短期訪(fǎng)問(wèn)量增大的情況,我們可以改變一下思路,也就是把固定的時(shí)間段改成動(dòng)態(tài)的:

假設(shè)某個(gè)接口在10秒內(nèi)只允許訪(fǎng)問(wèn)5次。用戶(hù)每次訪(fǎng)問(wèn)接口時(shí),記錄當(dāng)前用戶(hù)訪(fǎng)問(wèn)的時(shí)間點(diǎn)(時(shí)間戳),并計(jì)算前10秒內(nèi)用戶(hù)訪(fǎng)問(wèn)該接口的總次數(shù)。如果總次數(shù)大于限流次數(shù),則不允許用戶(hù)訪(fǎng)問(wèn)該接口。這樣就能保證在任意時(shí)刻用戶(hù)的訪(fǎng)問(wèn)次數(shù)不會(huì)超過(guò)1000次。

如下圖,假設(shè)用戶(hù)在0:19時(shí)間點(diǎn)訪(fǎng)問(wèn)接口,經(jīng)檢查其前10秒內(nèi)訪(fǎng)問(wèn)次數(shù)為5次,則允許本次訪(fǎng)問(wèn)。

圖片

假設(shè)用戶(hù)0:20時(shí)間點(diǎn)訪(fǎng)問(wèn)接口,經(jīng)檢查其前10秒內(nèi)訪(fǎng)問(wèn)次數(shù)為6次(超出限流次數(shù)5次),則不允許本次訪(fǎng)問(wèn)。

圖片

1.2.2 Redis部分的實(shí)現(xiàn)

1)選用何種 Redis 數(shù)據(jù)結(jié)構(gòu)

首先是需要確定使用哪個(gè)Redis數(shù)據(jù)結(jié)構(gòu)。用戶(hù)每次訪(fǎng)問(wèn)時(shí),需要用一個(gè)key記錄用戶(hù)訪(fǎng)問(wèn)的時(shí)間點(diǎn),而且還需要利用這些時(shí)間點(diǎn)進(jìn)行范圍檢查。

2)為何選擇 zSet 數(shù)據(jù)結(jié)構(gòu)

為了能夠?qū)崿F(xiàn)范圍檢查,可以考慮使用Redis中的zSet有序集合。

添加一個(gè)zSet元素的命令如下:

ZADD?[key]?[score]?[member]

它有一個(gè)關(guān)鍵的屬性score,通過(guò)它可以記錄當(dāng)前member的優(yōu)先級(jí)。

于是我們可以把score設(shè)置成用戶(hù)訪(fǎng)問(wèn)接口的時(shí)間戳,以便于通過(guò)score進(jìn)行范圍檢查。key則記錄用戶(hù)IP和接口方法名,至于member設(shè)置成什么沒(méi)有影響,一個(gè)member記錄了用戶(hù)訪(fǎng)問(wèn)接口的時(shí)間點(diǎn)。因此member也可以設(shè)置成時(shí)間戳。

3)zSet 如何進(jìn)行范圍檢查(檢查前幾秒的訪(fǎng)問(wèn)次數(shù))

思路是,把特定時(shí)間間隔之前的member都刪掉,留下的member就是時(shí)間間隔之內(nèi)的總訪(fǎng)問(wèn)次數(shù)。然后統(tǒng)計(jì)當(dāng)前key中的member有多少個(gè)即可。

① 把特定時(shí)間間隔之前的member都刪掉。

zSet有如下命令,用于刪除score范圍在[min~max]之間的member:

Zremrangebyscore?[key]?[min]?[max]

假設(shè)限流時(shí)間設(shè)置為5秒,當(dāng)前用戶(hù)訪(fǎng)問(wèn)接口時(shí),獲取當(dāng)前系統(tǒng)時(shí)間戳為currentTimeMill,那么刪除的score范圍可以設(shè)置為:

min = 0
max = currentTimeMill - 5 * 1000

相當(dāng)于把5秒之前的所有member都刪除了,只留下前5秒內(nèi)的key。

② 統(tǒng)計(jì)特定key中已存在的member有多少個(gè)。

zSet有如下命令,用于統(tǒng)計(jì)某個(gè)key的member總數(shù):

?ZCARD?[key]

統(tǒng)計(jì)的key的member總數(shù),就是當(dāng)前接口已經(jīng)訪(fǎng)問(wèn)的次數(shù)。如果該數(shù)目大于限流次數(shù),則說(shuō)明當(dāng)前的訪(fǎng)問(wèn)應(yīng)被限流。

二、代碼實(shí)現(xiàn)

主要是使用注解 + AOP的形式實(shí)現(xiàn)。

2.1 固定時(shí)間段思路

使用了lua腳本。

參考:http://m.fzitv.net/program/293608ct1.htm

2.1.1 限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
    /**
     * 限流時(shí)間,單位秒
     */
    int time() default 5;
    /**
     * 限流次數(shù)
     */
    int count() default 10;
}

2.1.2 定義lua腳本

resources/lua下新建limit.lua

-- 獲取redis鍵
local key = KEYS[1]
-- 獲取第一個(gè)參數(shù)(次數(shù))
local count = tonumber(ARGV[1])
-- 獲取第二個(gè)參數(shù)(時(shí)間)
local time = tonumber(ARGV[2])
-- 獲取當(dāng)前流量
local current = redis.call('get', key);
-- 如果current值存在,且值大于規(guī)定的次數(shù),則拒絕放行(直接返回當(dāng)前流量)
if current and tonumber(current) > count then
    return tonumber(current)
end
-- 如果值小于規(guī)定次數(shù),或值不存在,則允許放行,當(dāng)前流量數(shù)+1  (值不存在情況下,可以自增變?yōu)?)
current = redis.call('incr', key);
-- 如果是第一次進(jìn)來(lái),那么開(kāi)始設(shè)置鍵的過(guò)期時(shí)間。
if tonumber(current) == 1 then 
    redis.call('expire', key, time);
end
-- 返回當(dāng)前流量
return tonumber(current)

2.1.3 注入Lua執(zhí)行腳本

關(guān)鍵代碼是limitScript()方法

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化(默認(rèn)采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
    /**
     * 解析lua腳本的bean
     */
    @Bean("limitScript")
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

2.1.3 定義Aop切面類(lèi)

@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
 @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
 @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        String combineKey = getCombineKey(rateLimiter.type(), point);
        List<String> keys = Collections.singletonList(combineKey);
        try {
            Long number = (Long) redisTemplate.execute(limitScript, keys, count, time);
            // 當(dāng)前流量number已超過(guò)限制,則拋出異常
            if (number == null || number.intValue() > count) {
             throw new RuntimeException("訪(fǎng)問(wèn)過(guò)于頻繁,請(qǐng)稍后再試");
            }
            log.info("[limit] 限制請(qǐng)求數(shù)'{}',當(dāng)前請(qǐng)求數(shù)'{}',緩存key'{}'", count, number.intValue(), combineKey);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("服務(wù)器限流異常,請(qǐng)稍候再試");
        }
    }
    /**
     * 把用戶(hù)IP和接口方法名拼接成 redis 的 key
     * @param point 切入點(diǎn)
     * @return 組合key
     */
    private String getCombineKey(JoinPoint point) {
        StringBuilder sb = new StringBuilder("rate_limit:");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // keyPrefix + "-" + class + "-" + method
        return sb.append("-").append( targetClass.getName() )
                .append("-").append(method.getName()).toString();
    }
}

2.2 滑動(dòng)窗口思路

2.2.1 限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
    /**
     * 限流時(shí)間,單位秒
     */
    int time() default 5;
    /**
     * 限流次數(shù)
     */
    int count() default 10;
}

2.2.2 定義Aop切面類(lèi)

@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 實(shí)現(xiàn)限流(新思路)
     * @param point
     * @param rateLimiter
     * @throws Throwable
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        // 在 {time} 秒內(nèi)僅允許訪(fǎng)問(wèn) {count} 次。
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        // 根據(jù)用戶(hù)IP(可選)和接口方法,構(gòu)造key
        String combineKey = getCombineKey(rateLimiter.type(), point);
        // 限流邏輯實(shí)現(xiàn)
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        // 記錄本次訪(fǎng)問(wèn)的時(shí)間結(jié)點(diǎn)
        long currentMs = System.currentTimeMillis();
        zSetOperations.add(combineKey, currentMs, currentMs);
        // 這一步是為了防止member一直存在于內(nèi)存中
        redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);
        // 移除{time}秒之前的訪(fǎng)問(wèn)記錄(滑動(dòng)窗口思想)
        zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000);
        // 獲得當(dāng)前窗口內(nèi)的訪(fǎng)問(wèn)記錄數(shù)
        Long currCount = zSetOperations.zCard(combineKey);
        // 限流判斷
        if (currCount > count) {
            log.error("[limit] 限制請(qǐng)求數(shù)'{}',當(dāng)前請(qǐng)求數(shù)'{}',緩存key'{}'", count, currCount, combineKey);
            throw new RuntimeException("訪(fǎng)問(wèn)過(guò)于頻繁,請(qǐng)稍后再試!");
        }
    }
    /**
     * 把用戶(hù)IP和接口方法名拼接成 redis 的 key
     * @param point 切入點(diǎn)
     * @return 組合key
     */
    private String getCombineKey(JoinPoint point) {
        StringBuilder sb = new StringBuilder("rate_limit:");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // keyPrefix + "-" + class + "-" + method
        return sb.append("-").append( targetClass.getName() )
                .append("-").append(method.getName()).toString();
    }
}

到此這篇關(guān)于SpringBoot使用Redis對(duì)用戶(hù)IP進(jìn)行接口限流的文章就介紹到這了,更多相關(guān)SpringBoot Redis接口限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • SpringBoot中常用數(shù)據(jù)處理方式詳解

    SpringBoot中常用數(shù)據(jù)處理方式詳解

    這篇文章主要為大家詳細(xì)介紹了SpringBoot中常用數(shù)據(jù)處理方式的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-07-07
  • 一文徹底搞懂Java?BIO、NIO、AIO的核心區(qū)別

    一文徹底搞懂Java?BIO、NIO、AIO的核心區(qū)別

    這篇文章主要介紹了Java?BIO、NIO、AIO核心區(qū)別的相關(guān)資料,BIO、NIO、AIO是Java中三種核心的IO模型,本質(zhì)是操作系統(tǒng)層面 IO操作的不同處理方式,需要的朋友可以參考下
    2026-01-01
  • 淺談springboot之JoinPoint的getSignature方法

    淺談springboot之JoinPoint的getSignature方法

    這篇文章主要介紹了springboot之JoinPoint的getSignature方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-06-06
  • springmvc實(shí)現(xiàn)導(dǎo)出數(shù)據(jù)信息為excle表格示例代碼

    springmvc實(shí)現(xiàn)導(dǎo)出數(shù)據(jù)信息為excle表格示例代碼

    本篇文章主要介紹了springmvc實(shí)現(xiàn)導(dǎo)出數(shù)據(jù)信息為excle表格,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧。
    2017-01-01
  • 詳解Springboot2.3集成Spring security 框架(原生集成)

    詳解Springboot2.3集成Spring security 框架(原生集成)

    這篇文章主要介紹了詳解Springboot2.3集成Spring security 框架(原生集成),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-08-08
  • MyBatis獲取參數(shù)值的兩種方式詳解

    MyBatis獲取參數(shù)值的兩種方式詳解

    本文主要介紹了MyBatis獲取參數(shù)值的兩種方式詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • SpringBoot開(kāi)啟虛擬線(xiàn)程的實(shí)現(xiàn)流程

    SpringBoot開(kāi)啟虛擬線(xiàn)程的實(shí)現(xiàn)流程

    虛擬線(xiàn)程(Virtual?Thread)也稱(chēng)協(xié)程或纖程,是一種輕量級(jí)的線(xiàn)程實(shí)現(xiàn),與傳統(tǒng)的線(xiàn)程以及操作系統(tǒng)級(jí)別的線(xiàn)程(也稱(chēng)為平臺(tái)線(xiàn)程)相比,它的創(chuàng)建開(kāi)銷(xiāo)更小、資源利用率更高,本文給大家介紹了SpringBoot如何開(kāi)啟虛擬線(xiàn)程,需要的朋友可以參考下
    2024-06-06
  • Java中創(chuàng)建線(xiàn)程的四種方法解析

    Java中創(chuàng)建線(xiàn)程的四種方法解析

    這篇文章主要介紹了Java中創(chuàng)建線(xiàn)程的四種方法解析,線(xiàn)程是Java編程語(yǔ)言中的一個(gè)重要概念,它允許程序在同一時(shí)間執(zhí)行多個(gè)任務(wù),線(xiàn)程是程序中的執(zhí)行路徑,可以同時(shí)執(zhí)行多個(gè)線(xiàn)程,每個(gè)線(xiàn)程都有自己的執(zhí)行流程,需要的朋友可以參考下
    2023-10-10
  • Spring之異步任務(wù)@Async解讀

    Spring之異步任務(wù)@Async解讀

    這篇文章主要介紹了Spring之異步任務(wù)@Async,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • Java獲取mac地址的方法

    Java獲取mac地址的方法

    這篇文章主要介紹了Java獲取mac地址的方法,涉及java針對(duì)系統(tǒng)硬件及IO操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-07-07

最新評(píng)論

九寨沟县| 八宿县| 兴义市| 遂宁市| 辽阳市| 阿鲁科尔沁旗| 通榆县| 沈丘县| 米林县| 新建县| 平和县| 彭山县| 县级市| 秦安县| 靖边县| 望奎县| 宁波市| 五家渠市| 宣城市| 商都县| 宜兰市| 娱乐| 德江县| 登封市| 唐山市| 丹寨县| 成武县| 临漳县| 华容县| 灯塔市| 沁阳市| 东光县| 乐陵市| 尼木县| 怀仁县| 乌拉特后旗| 南汇区| 博罗县| 盐亭县| 西和县| 广平县|